diff --git a/cmd/identities/get_test.go b/cmd/identities/get_test.go index d894484b469c..7520b011ed14 100644 --- a/cmd/identities/get_test.go +++ b/cmd/identities/get_test.go @@ -79,6 +79,9 @@ func TestGetCmd(t *testing.T) { InitialAccessToken: transform(accessToken + "0"), InitialRefreshToken: transform(refreshToken + "0"), InitialIDToken: transform(idToken + "0"), + LastAccessToken: transform(accessToken + "0"), + LastRefreshToken: transform(refreshToken + "0"), + LastIDToken: transform(idToken + "0"), Organization: "foo-org-id", }, { @@ -87,6 +90,9 @@ func TestGetCmd(t *testing.T) { InitialAccessToken: transform(accessToken + "1"), InitialRefreshToken: transform(refreshToken + "1"), InitialIDToken: transform(idToken + "1"), + LastAccessToken: transform(accessToken + "1"), + LastRefreshToken: transform(refreshToken + "1"), + LastIDToken: transform(idToken + "1"), Organization: "bar-org-id", }, }}), diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 51ee64ae7a7d..239008a74f66 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -685,6 +685,14 @@ "examples": [ "https://example.com" ] + }, + "capture_last_tokens": { + "title": "Capture last received login tokens", + "description": "If true, kratos will store the most recent token data recieved on login in the DB", + "type": "boolean", + "examples": [ + true + ] } }, "additionalProperties": false, diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json index 5edf83e8e9f7..ee4fef538fde 100644 --- a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json @@ -9,14 +9,20 @@ "provider": "google", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject": "import-2", "provider": "github", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }, @@ -44,14 +50,20 @@ "provider": "okta", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject": "import-saml-2", "provider": "onelogin", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }, diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_organization_oidc_and_saml_credentials.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_organization_oidc_and_saml_credentials.json index c53957d6f71c..ba5852da4c5f 100644 --- a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_organization_oidc_and_saml_credentials.json +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_organization_oidc_and_saml_credentials.json @@ -10,6 +10,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "ad6a7dac-4eef-4f09-8e58-c099c14b6c36" }, { @@ -18,6 +21,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "ad6a7dac-4eef-4f09-8e58-c099c14b6c36" } ] @@ -46,6 +52,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "ad6a7dac-4eef-4f09-8e58-c099c14b6c36" }, { @@ -54,6 +63,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "ad6a7dac-4eef-4f09-8e58-c099c14b6c36" } ] diff --git a/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json b/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json index a9624ceed9b4..e9aeb318916a 100644 --- a/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json +++ b/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json @@ -1 +1 @@ -"{\"providers\":[{\"initial_id_token\":\"id_token0\",\"initial_access_token\":\"access_token0\",\"initial_refresh_token\":\"refresh_token0\",\"subject\":\"foo\",\"provider\":\"bar\"},{\"initial_id_token\":\"id_token1\",\"initial_access_token\":\"access_token1\",\"initial_refresh_token\":\"refresh_token1\",\"subject\":\"baz\",\"provider\":\"zab\"}]}" +"{\"providers\":[{\"initial_id_token\":\"id_token0\",\"initial_access_token\":\"access_token0\",\"initial_refresh_token\":\"refresh_token0\",\"last_id_token\":\"id_token_current0\",\"last_access_token\":\"access_token_current0\",\"last_refresh_token\":\"refresh_token_current0\",\"subject\":\"foo\",\"provider\":\"bar\"},{\"initial_id_token\":\"id_token1\",\"initial_access_token\":\"access_token1\",\"initial_refresh_token\":\"refresh_token1\",\"last_id_token\":\"id_token_current1\",\"last_access_token\":\"access_token_current1\",\"last_refresh_token\":\"refresh_token_current1\",\"subject\":\"baz\",\"provider\":\"zab\"}]}" diff --git a/identity/.snapshots/TestImportCredentials-OIDC_new_credential_with_organization.json b/identity/.snapshots/TestImportCredentials-OIDC_new_credential_with_organization.json index f33b9ef83074..923466897d87 100644 --- a/identity/.snapshots/TestImportCredentials-OIDC_new_credential_with_organization.json +++ b/identity/.snapshots/TestImportCredentials-OIDC_new_credential_with_organization.json @@ -11,6 +11,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" } ] diff --git a/identity/.snapshots/TestImportCredentials-OIDC_new_credential_without_organization.json b/identity/.snapshots/TestImportCredentials-OIDC_new_credential_without_organization.json index d22cba0eae5c..88508142ee4c 100644 --- a/identity/.snapshots/TestImportCredentials-OIDC_new_credential_without_organization.json +++ b/identity/.snapshots/TestImportCredentials-OIDC_new_credential_without_organization.json @@ -10,7 +10,10 @@ "provider": "github", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }, diff --git a/identity/.snapshots/TestImportCredentials-OIDC_update_credential_with_organization.json b/identity/.snapshots/TestImportCredentials-OIDC_update_credential_with_organization.json index 8992fa21e167..5e3416a6af1c 100644 --- a/identity/.snapshots/TestImportCredentials-OIDC_update_credential_with_organization.json +++ b/identity/.snapshots/TestImportCredentials-OIDC_update_credential_with_organization.json @@ -11,7 +11,10 @@ "provider": "google", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject": "12345", @@ -19,6 +22,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" } ] diff --git a/identity/.snapshots/TestImportCredentials-OIDC_update_credential_without_organization.json b/identity/.snapshots/TestImportCredentials-OIDC_update_credential_without_organization.json index c227d75fdf1f..f4897a190688 100644 --- a/identity/.snapshots/TestImportCredentials-OIDC_update_credential_without_organization.json +++ b/identity/.snapshots/TestImportCredentials-OIDC_update_credential_without_organization.json @@ -11,14 +11,20 @@ "provider": "google", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject": "12345", "provider": "github", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }, diff --git a/identity/.snapshots/TestImportCredentials-OIDC_update_with_multiple_providers.json b/identity/.snapshots/TestImportCredentials-OIDC_update_with_multiple_providers.json index 3d8779c9b0db..b24985b1c5f8 100644 --- a/identity/.snapshots/TestImportCredentials-OIDC_update_with_multiple_providers.json +++ b/identity/.snapshots/TestImportCredentials-OIDC_update_with_multiple_providers.json @@ -12,7 +12,10 @@ "provider": "google", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject": "12345", @@ -20,6 +23,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" }, { @@ -27,7 +33,10 @@ "provider": "gitlab", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }, diff --git a/identity/.snapshots/TestImportCredentials-SAML_new_credential_with_organization.json b/identity/.snapshots/TestImportCredentials-SAML_new_credential_with_organization.json index 2d67d7816171..ed65b4bbb09c 100644 --- a/identity/.snapshots/TestImportCredentials-SAML_new_credential_with_organization.json +++ b/identity/.snapshots/TestImportCredentials-SAML_new_credential_with_organization.json @@ -11,6 +11,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" } ] diff --git a/identity/.snapshots/TestImportCredentials-SAML_new_credential_without_organization.json b/identity/.snapshots/TestImportCredentials-SAML_new_credential_without_organization.json index 934659cc82b8..4318f8e75f1d 100644 --- a/identity/.snapshots/TestImportCredentials-SAML_new_credential_without_organization.json +++ b/identity/.snapshots/TestImportCredentials-SAML_new_credential_without_organization.json @@ -10,7 +10,10 @@ "provider": "okta", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }, diff --git a/identity/.snapshots/TestImportCredentials-SAML_update_credential_with_organization.json b/identity/.snapshots/TestImportCredentials-SAML_update_credential_with_organization.json index c79f1f79e5fe..b3f2033421dc 100644 --- a/identity/.snapshots/TestImportCredentials-SAML_update_credential_with_organization.json +++ b/identity/.snapshots/TestImportCredentials-SAML_update_credential_with_organization.json @@ -11,7 +11,10 @@ "provider": "onelogin", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject": "user123", @@ -19,6 +22,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" } ] diff --git a/identity/.snapshots/TestImportCredentials-SAML_update_credential_without_organization.json b/identity/.snapshots/TestImportCredentials-SAML_update_credential_without_organization.json index 1e3e1a2832bf..3a356dbe4b90 100644 --- a/identity/.snapshots/TestImportCredentials-SAML_update_credential_without_organization.json +++ b/identity/.snapshots/TestImportCredentials-SAML_update_credential_without_organization.json @@ -11,14 +11,20 @@ "provider": "onelogin", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject": "user123", "provider": "okta", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }, diff --git a/identity/.snapshots/TestImportCredentials-SAML_update_with_multiple_providers.json b/identity/.snapshots/TestImportCredentials-SAML_update_with_multiple_providers.json index d5bdd5db6f04..11dde2f55911 100644 --- a/identity/.snapshots/TestImportCredentials-SAML_update_with_multiple_providers.json +++ b/identity/.snapshots/TestImportCredentials-SAML_update_with_multiple_providers.json @@ -12,7 +12,10 @@ "provider": "onelogin", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject": "user123", @@ -20,6 +23,9 @@ "initial_id_token": "", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "organization": "e7e3cbae-04cc-45f3-ae52-ea749a2ffaff" }, { @@ -27,7 +33,10 @@ "provider": "auth0", "initial_id_token": "", "initial_access_token": "", - "initial_refresh_token": "" + "initial_refresh_token": "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }, diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json index 935daa2bbb55..911f2966f8c7 100644 --- a/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json @@ -10,6 +10,9 @@ "initial_id_token": "foo", "initial_access_token": "", "initial_refresh_token": "", + "last_id_token": "foo", + "last_access_token": "", + "last_refresh_token": "", "subject": "bar", "provider": "oidc1" } diff --git a/identity/credentials.go b/identity/credentials.go index 3453341d5a16..7c804ee9ea93 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -4,11 +4,15 @@ package identity import ( + "bytes" "context" "database/sql" + "encoding/json" "reflect" "time" + "github.com/pkg/errors" + "github.com/gofrs/uuid" "github.com/wI2L/jsondiff" @@ -191,6 +195,10 @@ func (c Credentials) GetID() uuid.UUID { return c.ID } +func (c Credentials) UnmarshalConfig(target interface{}) error { + return errors.WithStack(json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&target)) +} + type ( // swagger:ignore CredentialIdentifier struct { diff --git a/identity/credentials_oidc.go b/identity/credentials_oidc.go index 8a8ab1c113ca..03f6db49163d 100644 --- a/identity/credentials_oidc.go +++ b/identity/credentials_oidc.go @@ -20,7 +20,7 @@ type CredentialsOIDC struct { Providers []CredentialsOIDCProvider `json:"providers"` } -// CredentialsOIDCProvider is contains a specific OpenID COnnect credential for a particular connection (e.g. Google). +// CredentialsOIDCProvider is contains a specific OpenID Connect credential for a particular connection (e.g. Google). // // swagger:model identityCredentialsOidcProvider type CredentialsOIDCProvider struct { @@ -29,6 +29,9 @@ type CredentialsOIDCProvider struct { InitialIDToken string `json:"initial_id_token"` InitialAccessToken string `json:"initial_access_token"` InitialRefreshToken string `json:"initial_refresh_token"` + LastIDToken string `json:"last_id_token"` + LastAccessToken string `json:"last_access_token"` + LastRefreshToken string `json:"last_refresh_token"` Organization string `json:"organization,omitempty"` UseAutoLink bool `json:"use_auto_link,omitzero"` } @@ -85,6 +88,9 @@ func NewOIDCLikeCredentials(tokens *CredentialsOIDCEncryptedTokens, t Credential InitialIDToken: tokens.GetIDToken(), InitialAccessToken: tokens.GetAccessToken(), InitialRefreshToken: tokens.GetRefreshToken(), + LastIDToken: tokens.GetIDToken(), + LastAccessToken: tokens.GetAccessToken(), + LastRefreshToken: tokens.GetRefreshToken(), Organization: organization, }, }, diff --git a/identity/handler_test.go b/identity/handler_test.go index f5e765e5ab3a..e94bd8beb56c 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -525,6 +525,9 @@ func TestHandler(t *testing.T) { InitialAccessToken: transform(accessToken, "0"), InitialRefreshToken: transform(refreshToken, "0"), InitialIDToken: transform(idToken, "0"), + LastAccessToken: transform(accessToken, "_current0"), + LastRefreshToken: transform(refreshToken, "_current0"), + LastIDToken: transform(idToken, "_current0"), }, { Subject: "baz", @@ -532,6 +535,9 @@ func TestHandler(t *testing.T) { InitialAccessToken: transform(accessToken, "1"), InitialRefreshToken: transform(refreshToken, "1"), InitialIDToken: transform(idToken, "1"), + LastAccessToken: transform(accessToken, "_current1"), + LastRefreshToken: transform(refreshToken, "_current1"), + LastIDToken: transform(idToken, "_current1"), }, }}), }, @@ -672,11 +678,17 @@ func TestHandler(t *testing.T) { assert.EqualValues(t, "access_token0", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "refresh_token0", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "id_token0", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "access_token_current0", res.Get("credentials.oidc.config.providers.0.last_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "refresh_token_current0", res.Get("credentials.oidc.config.providers.0.last_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "id_token_current0", res.Get("credentials.oidc.config.providers.0.last_id_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "baz", res.Get("credentials.oidc.config.providers.1.subject").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "zab", res.Get("credentials.oidc.config.providers.1.provider").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "access_token1", res.Get("credentials.oidc.config.providers.1.initial_access_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "refresh_token1", res.Get("credentials.oidc.config.providers.1.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "id_token1", res.Get("credentials.oidc.config.providers.1.initial_id_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "access_token_current1", res.Get("credentials.oidc.config.providers.1.last_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "refresh_token_current1", res.Get("credentials.oidc.config.providers.1.last_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "id_token_current1", res.Get("credentials.oidc.config.providers.1.last_id_token").String(), "credentials should be included: %s", res.Raw) }) } }) @@ -732,11 +744,17 @@ func TestHandler(t *testing.T) { assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.last_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.last_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.last_id_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "baz", res.Get("credentials.oidc.config.providers.1.subject").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "zab", res.Get("credentials.oidc.config.providers.1.provider").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_access_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_id_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.last_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.last_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.last_id_token").String(), "credentials should be included: %s", res.Raw) }) } }) @@ -796,6 +814,9 @@ func TestHandler(t *testing.T) { assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "%s", res.Raw) assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "%s", res.Raw) assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.last_access_token").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.last_id_token").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.last_refresh_token").String(), "%s", res.Raw) }) } }) diff --git a/identity/identity.go b/identity/identity.go index 5aabad12a47c..f9970cc17fd3 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -489,7 +489,7 @@ func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Pro gjson.GetBytes(original.Config, "providers").ForEach(func(_, v gjson.Result) bool { if ct == CredentialsTypeOIDC { // Don't expose these for SAML - for _, token := range []string{"initial_id_token", "initial_access_token", "initial_refresh_token"} { + for _, token := range []string{"initial_id_token", "initial_access_token", "initial_refresh_token", "last_id_token", "last_access_token", "last_refresh_token"} { key := fmt.Sprintf("%d.%s", i, token) ciphertext := v.Get(token).String() diff --git a/identity/identity_test.go b/identity/identity_test.go index 3d33bc40ba81..878e6a4c2ef3 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -365,7 +365,7 @@ func TestWithDeclassifiedCredentials(t *testing.T) { Identifiers: []string{"bar", "baz"}, // hint: // echo '666f6f' | xxd -r -p - Config: sqlxx.JSONRawMessage(`{"providers": [{"subject":"bar","provider":"oidc1","initial_id_token":"666f6f"}]}`), + Config: sqlxx.JSONRawMessage(`{"providers": [{"subject":"bar","provider":"oidc1","initial_id_token":"666f6f","last_id_token": "666f6f"}]}`), }, CredentialsTypeSAML: { Type: CredentialsTypeSAML, @@ -589,26 +589,38 @@ func TestMergeOIDCCredentials(t *testing.T) { "provider" : "dont-touch", "initial_id_token" : "", "initial_access_token" : "", - "initial_refresh_token" : "" + "initial_refresh_token" : "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject" : "bar", "provider" : "also-dont-touch", "initial_id_token" : "", "initial_access_token" : "", "initial_refresh_token" : "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "", "use_auto_link": true }, { "subject" : "dont-replace", "provider" : "replace", "initial_id_token" : "", "initial_access_token" : "", - "initial_refresh_token" : "" + "initial_refresh_token" : "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" }, { "subject" : "new-subject", "provider" : "replace", "initial_id_token" : "", "initial_access_token" : "", - "initial_refresh_token" : "" + "initial_refresh_token" : "", + "last_id_token": "", + "last_access_token": "", + "last_refresh_token": "" } ] }`), }, diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index b366d8dc4a2a..735b0fd5f61f 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -137,6 +137,10 @@ type Configuration struct { // NetIDTokenOriginHeader contains the orgin header to be used when exchanging a // NetID FedCM token for an ID token. NetIDTokenOriginHeader string `json:"net_id_token_origin_header"` + + // CaptureLastTokens determines if tokens should be recaptured on every login. + // As this can be a very time intensive process, it is disabled by default. + CaptureLastTokens bool `json:"capture_last_tokens"` } func (p Configuration) Redir(public *url.URL) string { diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 62867b886a7a..6fec0225bee7 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -867,6 +867,9 @@ func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, to InitialAccessToken: tokens.GetAccessToken(), InitialRefreshToken: tokens.GetRefreshToken(), InitialIDToken: tokens.GetIDToken(), + LastAccessToken: tokens.GetAccessToken(), + LastRefreshToken: tokens.GetRefreshToken(), + LastIDToken: tokens.GetIDToken(), Organization: organization, }) diff --git a/selfservice/strategy/oidc/strategy_helper_test.go b/selfservice/strategy/oidc/strategy_helper_test.go index 2c25e7435ff0..26381ae6606e 100644 --- a/selfservice/strategy/oidc/strategy_helper_test.go +++ b/selfservice/strategy/oidc/strategy_helper_test.go @@ -278,6 +278,7 @@ func newHydra(t *testing.T, subject *string, claims *idTokenClaims, scope *[]str fmt.Sprintf("URLS_SELF_ISSUER=http://localhost:%d/", publicPort), "URLS_LOGIN=" + hydraIntegrationTSURL + "/login", "URLS_CONSENT=" + hydraIntegrationTSURL + "/consent", + "TTL_ACCESS_TOKEN=1s", "LOG_LEAK_SENSITIVE_VALUES=true", "SECRETS_SYSTEM=someverylongsecretthatis32byteslong", }, @@ -291,7 +292,7 @@ func newHydra(t *testing.T, subject *string, claims *idTokenClaims, scope *[]str t.Cleanup(func() { require.NoError(t, hydra.Close()) }) - require.NoError(t, hydra.Expire(uint(60*5))) + require.NoError(t, hydra.Expire(uint(60*10))) require.NotEmpty(t, hydra.GetPort("4444/tcp"), "%+v", hydra.Container.NetworkSettings.Ports) require.NotEmpty(t, hydra.GetPort("4445/tcp"), "%+v", hydra.Container) diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 23ec8a2e2e13..aa69f2ff81d8 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -99,7 +99,7 @@ type UpdateLoginFlowWithOidcMethod struct { TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s *Strategy) handleConflictingIdentity(ctx context.Context, w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (verdict ConflictingIdentityVerdict, id *identity.Identity, credentials *identity.Credentials, err error) { +func (s *Strategy) handleConflictingIdentity(ctx context.Context, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (verdict ConflictingIdentityVerdict, id *identity.Identity, credentials *identity.Credentials, err error) { if s.conflictingIdentityPolicy == nil { return ConflictingIdentityVerdictReject, nil, nil, nil } @@ -163,11 +163,14 @@ func (s *Strategy) ProcessLogin(ctx context.Context, w http.ResponseWriter, r *h ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.processLogin") defer otelx.End(span, &err) + // Determine if a merge occurred in this flow + merge := false i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), identity.OIDCUniqueID(provider.Config().ID, claims.Subject)) if err != nil { if errors.Is(err, sqlcon.ErrNoRows) { var verdict ConflictingIdentityVerdict - verdict, i, c, err = s.handleConflictingIdentity(ctx, w, r, loginFlow, token, claims, provider, container) + verdict, i, c, err = s.handleConflictingIdentity(ctx, token, claims, provider, container) + merge = true switch verdict { case ConflictingIdentityVerdictMerge: // Do nothing @@ -245,8 +248,17 @@ func (s *Strategy) ProcessLogin(ctx context.Context, w http.ResponseWriter, r *h sess := session.NewInactiveSession() sess.CompletedLoginForWithProvider(s.ID(), identity.AuthenticatorAssuranceLevel1, provider.Config().ID, provider.Config().OrganizationID) - for _, c := range oidcCredentials.Providers { - if c.Subject == claims.Subject && c.Provider == provider.Config().ID { + for index, p := range oidcCredentials.Providers { + if p.Subject == claims.Subject && p.Provider == provider.Config().ID { + + // Update the OIDC credentials unless we just merged as tokens will already be captured + if !merge && provider.Config().CaptureLastTokens { + i, _, err = s.handleCapturingTokens(ctx, token, oidcCredentials, index, c, i) + if err != nil { + return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) + } + } + if err = s.d.LoginHookExecutor().PostLoginHook(w, r, node.OpenIDConnectGroup, loginFlow, i, sess, provider.Config().ID); err != nil { return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) } @@ -257,6 +269,21 @@ func (s *Strategy) ProcessLogin(ctx context.Context, w http.ResponseWriter, r *h return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to find matching OpenID Connect credentials.").WithDebugf(`Unable to find credentials that match the given provider "%s" and subject "%s".`, provider.Config().ID, claims.Subject))) } +func (s *Strategy) handleCapturingTokens(ctx context.Context, token *identity.CredentialsOIDCEncryptedTokens, oidcCredentials identity.CredentialsOIDC, index int, c *identity.Credentials, i *identity.Identity) (id *identity.Identity, credentials *identity.Credentials, err error) { + oidcCredentials.Providers[index].LastIDToken = token.GetIDToken() + oidcCredentials.Providers[index].LastAccessToken = token.GetAccessToken() + oidcCredentials.Providers[index].LastRefreshToken = token.GetRefreshToken() + c.Config, err = json.Marshal(oidcCredentials) + if err != nil { + return nil, nil, err + } + i.SetCredentials(identity.CredentialsTypeOIDC, *c) + if err = s.d.PrivilegedIdentityPool().UpdateIdentity(ctx, i); err != nil { + return nil, nil, err + } + return i, c, nil +} + func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (i *identity.Identity, err error) { ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.oidc.Strategy.Login") defer otelx.End(span, &err) diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index 44dab1d3a7b4..6666bbd01875 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -261,6 +261,9 @@ func TestSettingsStrategy(t *testing.T) { assert.NotEmpty(t, p.InitialIDToken) assert.NotEmpty(t, p.InitialAccessToken) assert.NotEmpty(t, p.InitialRefreshToken) + assert.NotEmpty(t, p.LastIDToken) + assert.NotEmpty(t, p.LastAccessToken) + assert.NotEmpty(t, p.LastRefreshToken) } break } diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 7672dda900d1..03fde4ce3174 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -434,7 +434,7 @@ func TestStrategy(t *testing.T) { t, json.RawMessage(fmt.Sprintf(`{"providers": [{"subject":"%s","provider":"%s"}]}`, subject, provider)), json.RawMessage(c), - []string{"providers.0.initial_id_token", "providers.0.initial_access_token", "providers.0.initial_refresh_token"}, + []string{"providers.0.initial_id_token", "providers.0.initial_access_token", "providers.0.initial_refresh_token", "providers.0.last_id_token", "providers.0.last_access_token", "providers.0.last_refresh_token"}, ) } @@ -480,7 +480,7 @@ func TestStrategy(t *testing.T) { t, json.RawMessage(fmt.Sprintf(`{"providers": [{"subject":"%s","provider":"%s"}]}`, subject, provider)), json.RawMessage(c), - []string{"providers.0.initial_id_token", "providers.0.initial_access_token", "providers.0.initial_refresh_token"}, + []string{"providers.0.initial_id_token", "providers.0.initial_access_token", "providers.0.initial_refresh_token", "providers.0.last_id_token", "providers.0.last_access_token", "providers.0.last_refresh_token"}, ) return id } @@ -661,6 +661,18 @@ func TestStrategy(t *testing.T) { subject = "register-then-login@ory.sh" scope = []string{"openid", "offline"} + getInitialAccessToken := func(t *testing.T, provider string, body []byte) string { + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "identity.id").String())) + require.NoError(t, err) + c := i.Credentials[identity.CredentialsTypeOIDC].Config + return gjson.GetBytes(c, "providers.0.initial_access_token").String() + } + getCurrentAccessToken := func(t *testing.T, provider string, body []byte) string { + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "identity.id").String())) + require.NoError(t, err) + c := i.Credentials[identity.CredentialsTypeOIDC].Config + return gjson.GetBytes(c, "providers.0.current_access_token").String() + } t.Run("case=should pass registration", func(t *testing.T) { transientPayload := `{"data": "registration"}` r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) @@ -688,7 +700,17 @@ func TestStrategy(t *testing.T) { postLoginWebhook.AssertTransientPayload(t, transientPayload) }) - + t.Run("case=token from login should not be the same", func(t *testing.T) { + transientPayload := `{"data": "login"}` + r := newBrowserLoginFlow(t, returnTS.URL, 20*time.Second) + action := assertFormValues(t, r.ID, "valid") + res, body := makeRequest(t, "valid", action, url.Values{ + "transient_payload": {transientPayload}, + }) + assertIdentity(t, res, body) + expectTokens(t, "valid", body) + assert.NotEqual(t, getCurrentAccessToken(t, "valid", body), getInitialAccessToken(t, "valid", body)) + }) t.Run("case=should pass double submit", func(t *testing.T) { // This test checks that the continuity manager uses a grace period to handle potential double-submit issues. // diff --git a/test/e2e/cypress/integration/profiles/oidc-provider/update.spec.ts b/test/e2e/cypress/integration/profiles/oidc-provider/update.spec.ts new file mode 100644 index 000000000000..2078162c5c58 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/oidc-provider/update.spec.ts @@ -0,0 +1,164 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, website } from "../../../helpers" +import { routes as express } from "../../../helpers/express" +import { routes as react } from "../../../helpers/react" + +context("OpenID Provider Update", () => { + ;[ + { + login: react.login, + registration: react.registration, + app: "react" as "react", + profile: "spa", + }, + { + login: express.login, + registration: express.registration, + app: "express" as "express", + profile: "oidc", + }, + ].forEach(({ login, registration, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + beforeEach(() => { + cy.clearAllCookies() + cy.visit(registration) + cy.setIdentitySchema( + "file://test/e2e/profiles/oidc/identity.traits.schema.json", + ) + }) + + const shouldSession = (email) => (session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.website).to.equal(website) + expect(identity.traits.email).to.equal(email) + } + + it("should be able to sign up, sign out, sign in and then check token", () => { + const email = gen.email() + + // sign up + cy.registerOidc({ + app, + email, + expectSession: false, + route: registration, + }) + + cy.get("#registration-password").should("not.exist") + cy.get(appPrefix(app) + '[name="traits.email"]').should( + "have.value", + email, + ) + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property website is missing", + ) + + cy.get('[name="traits.consent"][type="checkbox"]') + .siblings("label") + .click() + cy.get('[name="traits.newsletter"][type="checkbox"]') + .siblings("label") + .click() + cy.get('[name="traits.website"]').type("http://s") + + cy.get('[name="provider"]') + .should("have.length", 1) + .should("have.value", "hydra") + .should("contain.text", "Continue") + .click() + + cy.get("#registration-password").should("not.exist") + cy.get('[name="traits.email"]').should("have.value", email) + cy.get('[name="traits.website"]').should("have.value", "http://s") + cy.get('[data-testid="ui/message/4000003"]').should( + "contain.text", + "length must be >= 10", + ) + cy.get('[name="traits.website"]') + .should("have.value", "http://s") + .clear() + .type(website) + + cy.get('[name="traits.consent"]').should("be.checked") + cy.get('[name="traits.newsletter"]').should("be.checked") + + cy.triggerOidc(app) + + cy.location("pathname").should((loc) => { + expect(loc).to.be.oneOf(["/welcome", "/", "/sessions"]) + }) + + // sign out + + cy.logout() + cy.noSession() + + // sign in + cy.loginOidc({ app }) + + cy.location("pathname").should((loc) => { + expect(loc).to.be.oneOf(["/welcome", "/", "/sessions"]) + }) + cy.getSession().then((session) => { + shouldSession(email)(session) + cy.getFullIdentityById({ id: session.identity.id }).then( + (identity) => { + expect( + identity.credentials.oidc.config.providers[0] + .initial_access_token, + ).to.not.be.empty + expect( + identity.credentials.oidc.config.providers[0].initial_id_token, + ).to.not.be.empty + expect( + identity.credentials.oidc.config.providers[0] + .initial_refresh_token, + ).to.not.be.empty + expect( + identity.credentials.oidc.config.providers[0].last_access_token, + ).to.not.be.empty + expect( + identity.credentials.oidc.config.providers[0].last_id_token, + ).to.not.be.empty + expect( + identity.credentials.oidc.config.providers[0] + .last_refresh_token, + ).to.not.be.empty + + expect( + identity.credentials.oidc.config.providers[0] + .initial_access_token, + ).to.not.eq( + identity.credentials.oidc.config.providers[0].last_access_token, + ) + expect( + identity.credentials.oidc.config.providers[0].initial_id_token, + ).to.not.eq( + identity.credentials.oidc.config.providers[0].last_id_token, + ) + expect( + identity.credentials.oidc.config.providers[0] + .initial_refresh_token, + ).to.not.eq( + identity.credentials.oidc.config.providers[0] + .last_refresh_token, + ) + + expect( + identity.credentials.oidc.config.providers[0].provider, + ).to.eq("hydra") + }, + ) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index aaa8b4b81dfd..cd0eaa7c8049 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -1107,6 +1107,32 @@ Cypress.Commands.add("noSession", () => }), ) +Cypress.Commands.add("getIdentityByEmail", ({ email }) => + cy + .request({ + method: "GET", + url: `${KRATOS_ADMIN}/admin/identities`, + failOnStatusCode: false, + }) + .then((response) => { + expect(response.status).to.eq(200) + return response.body.find((identity) => identity.traits.email === email) + }), +) + +Cypress.Commands.add("getFullIdentityById", ({ id }) => + cy + .request({ + method: "GET", + url: `${KRATOS_ADMIN}/admin/identities/${id}?include_credential=oidc`, + failOnStatusCode: false, + }) + .then((response) => { + expect(response.status).to.eq(200) + return response.body + }), +) + Cypress.Commands.add( "performEmailVerification", ({ expect: { email, redirectTo }, strategy = "code" }) => { diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts index 667360b32c66..5467edaed5a4 100644 --- a/test/e2e/cypress/support/index.d.ts +++ b/test/e2e/cypress/support/index.d.ts @@ -690,6 +690,18 @@ declare global { */ setDefaultIdentitySchema(id: string): Chainable + /** + * Get identity from email + * @param email + */ + getIdentityByEmail(email: string): Chainable + + /** + * Get identity with credential from id + * @param id + */ + getFullIdentityById(id: string): Chainable + /** * Remove the specified attribute from the given HTML elements */ diff --git a/test/e2e/profiles/oidc/.kratos.yml b/test/e2e/profiles/oidc/.kratos.yml index b0a327bb5096..31a871c3471a 100644 --- a/test/e2e/profiles/oidc/.kratos.yml +++ b/test/e2e/profiles/oidc/.kratos.yml @@ -13,6 +13,7 @@ selfservice: scope: - offline mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + capture_last_tokens: true - id: google provider: generic client_id: ${OIDC_GOOGLE_CLIENT_ID} @@ -21,6 +22,7 @@ selfservice: scope: - offline mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + capture_last_tokens: true - id: github provider: generic client_id: ${OIDC_GITHUB_CLIENT_ID} @@ -29,6 +31,7 @@ selfservice: scope: - offline mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + capture_last_tokens: true flows: settings: diff --git a/test/e2e/profiles/spa/.kratos.yml b/test/e2e/profiles/spa/.kratos.yml index 6d5eb44a67de..8feb2947b78d 100644 --- a/test/e2e/profiles/spa/.kratos.yml +++ b/test/e2e/profiles/spa/.kratos.yml @@ -49,6 +49,7 @@ selfservice: scope: - offline mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + capture_last_tokens: true - id: google provider: generic @@ -58,6 +59,7 @@ selfservice: scope: - offline mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + capture_last_tokens: true - id: github provider: generic @@ -67,6 +69,7 @@ selfservice: scope: - offline mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + capture_last_tokens: true totp: enabled: true config: diff --git a/test/e2e/shared/config.d.ts b/test/e2e/shared/config.d.ts index 8da9f82bf159..c8e36cd169b5 100644 --- a/test/e2e/shared/config.d.ts +++ b/test/e2e/shared/config.d.ts @@ -69,6 +69,14 @@ export type LoginUIURL = string * The style of the login flow. If set to `unified` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials. */ export type LoginFlowStyle = "unified" | "identifier_first" +export type SelfServiceAfterDefaultLoginMethodHooks = ( + | SelfServiceSessionRevokerHook + | SelfServiceRequireVerifiedAddressHook + | SelfServiceWebHook + | SelfServiceVerificationHook + | SelfServiceShowVerificationUIHook + | B2BSSOHook +)[] /** * If set to true will enable [Email and Phone Verification and Account Activation](https://www.ory.sh/kratos/docs/self-service/flows/verify-email-account-activation/). */ @@ -233,6 +241,7 @@ export type SelfServiceOIDCProvider = SelfServiceOIDCProvider1 & { pkce?: ProofKeyForCodeExchange fedcm_config_url?: FederationConfigurationURL net_id_token_origin_header?: NetIDTokenOriginHeader + capture_last_tokens?: CaptureLastReceivedLoginTokens } export type SelfServiceOIDCProvider1 = { [k: string]: unknown | undefined @@ -261,6 +270,7 @@ export type Provider = | "netid" | "dingtalk" | "patreon" + | "line" | "linkedin" | "linkedin_v2" | "lark" @@ -313,6 +323,10 @@ export type FederationConfigurationURL = string * Contains the orgin header to be used when exchanging a NetID FedCM token for an ID token */ export type NetIDTokenOriginHeader = string +/** + * If true, kratos will store the most recent token data recieved on login in the DB + */ +export type CaptureLastReceivedLoginTokens = boolean /** * A list and configuration of OAuth2 and OpenID Connect providers Ory Kratos should integrate with. */ @@ -574,14 +588,26 @@ export type SetOrySessionEdgeCachingMaximumAge = string * If enabled allows new flow transitions using `continue_with` items. */ export type EnableNewFlowTransitionsUsingContinueWithItems = boolean +/** + * If true, restores the legacy behavior of always including `show_verification_ui` in the registration flow's `continue_with` when verification is enabled. If set to false, `show_verification_ui` is only set in `continue_with` if the `show_verification_ui` hook is used. This flag will be removed in the future. + */ +export type AlwaysIncludeShowVerificationUiInContinueWith = boolean +/** + * If true, the login flow will return a form error if the login identifier is not verified, which restores legacy behavior. If this value is false, the `continue_with` array will contain a `show_verification_ui` hook instead. + */ +export type ReturnAFormErrorIfTheLoginIdentifierIsNotVerified = boolean /** * If enabled allows faster session extension by skipping the session lookup. Disabling this feature will be deprecated in the future. */ export type EnableFasterSessionExtension = boolean /** - * The node group to use for registration flows. Previously, the node group for the password method's profile fields was `password`. Going forward, it will be `default`. This switch can toggle between those two for backwards compatibility + * The node group to use for registration flows. Previously, the node group for the password method's profile fields was `password`. Going forward, it will be `default`. This switch can toggle between those two for backwards compatibility. */ export type RegistrationNodeGroup = "password" | "default" +/** + * The node group to use for registration flows. Previously, the node group for the oidc method's profile fields was `oidc`. Going forward, it will be `default`. This switch can toggle between those two for backwards compatibility and will be removed in the future. + */ +export type RegistrationNodeGroupForOIDC = boolean /** * Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network. */ @@ -590,6 +616,10 @@ export type Organizations = unknown[] * A fallback URL template used when looking up identity schemas. */ export type FallbackURLTemplateForIdentitySchemas = string +/** + * Set a recognizable revision. This could be the commit time or a random value. This value is exposed at the `/health/config` endpoint and allows you to ensure that the correct config is loaded. + */ +export type ConfigRevision = string export interface OryKratosConfiguration2 { selfservice: { @@ -824,7 +854,7 @@ export interface SelfServiceAfterSettings { webauthn?: SelfServiceAfterSettingsAuthMethod passkey?: SelfServiceAfterSettingsAuthMethod lookup_secret?: SelfServiceAfterSettingsAuthMethod - profile?: SelfServiceAfterSettingsMethod + profile?: SelfServiceAfterSettingsProfileMethod hooks?: SelfServiceHooks } export interface SelfServiceAfterSettingsAuthMethod { @@ -838,9 +868,16 @@ export interface SelfServiceWebHook { export interface SelfServiceSessionRevokerHook { hook: "revoke_active_sessions" } -export interface SelfServiceAfterSettingsMethod { +export interface SelfServiceAfterSettingsProfileMethod { default_browser_return_url?: RedirectBrowsersToSetURLPerDefault - hooks?: (SelfServiceWebHook | B2BSSOHook)[] + hooks?: ( + | SelfServiceWebHook + | SelfServiceShowVerificationUIHook + | B2BSSOHook + )[] +} +export interface SelfServiceShowVerificationUIHook { + hook: "show_verification_ui" } export interface B2BSSOHook { hook: "b2b_sso" | "organization" @@ -875,9 +912,6 @@ export interface SelfServiceAfterRegistrationMethod { export interface SelfServiceSessionIssuerHook { hook: "session" } -export interface SelfServiceShowVerificationUIHook { - hook: "show_verification_ui" -} export interface SelfServiceBeforeLogin { hooks?: SelfServiceHooks } @@ -890,25 +924,11 @@ export interface SelfServiceAfterLogin { code?: SelfServiceAfterDefaultLoginMethod totp?: SelfServiceAfterDefaultLoginMethod lookup_secret?: SelfServiceAfterDefaultLoginMethod - hooks?: ( - | SelfServiceWebHook - | SelfServiceSessionRevokerHook - | SelfServiceRequireVerifiedAddressHook - | SelfServiceVerificationHook - | SelfServiceShowVerificationUIHook - | B2BSSOHook - )[] + hooks?: SelfServiceAfterDefaultLoginMethodHooks } export interface SelfServiceAfterDefaultLoginMethod { default_browser_return_url?: RedirectBrowsersToSetURLPerDefault - hooks?: ( - | SelfServiceSessionRevokerHook - | SelfServiceRequireVerifiedAddressHook - | SelfServiceWebHook - | SelfServiceVerificationHook - | SelfServiceShowVerificationUIHook - | B2BSSOHook - )[] + hooks?: SelfServiceAfterDefaultLoginMethodHooks } export interface SelfServiceRequireVerifiedAddressHook { hook: "require_verified_address" @@ -1016,6 +1036,10 @@ export interface PasswordConfiguration { */ emit_analytics_event?: boolean auth?: AuthMechanisms + /** + * URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads + */ + body?: string additionalProperties?: false } } @@ -1510,8 +1534,11 @@ export interface FeatureFlags { cacheable_sessions?: EnableOrySessionsCaching cacheable_sessions_max_age?: SetOrySessionEdgeCachingMaximumAge use_continue_with_transitions?: EnableNewFlowTransitionsUsingContinueWithItems + legacy_continue_with_verification_ui?: AlwaysIncludeShowVerificationUiInContinueWith + legacy_require_verified_login_error?: ReturnAFormErrorIfTheLoginIdentifierIsNotVerified faster_session_extend?: EnableFasterSessionExtension password_profile_registration_node_group?: RegistrationNodeGroup + legacy_oidc_registration_node_group?: RegistrationNodeGroupForOIDC } /** * Specifies enterprise features. Only effective in the Ory Network or with a valid license. @@ -1519,9 +1546,3 @@ export interface FeatureFlags { export interface EnterpriseFeatures { identity_schema_fallback_url_template?: FallbackURLTemplateForIdentitySchemas } -/** - * Only used in tests - */ -export interface ConfigRevision { - [k: string]: unknown | undefined -} diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullApple.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullApple.yaml index 1f0ee5f27665..be7b05aac727 100644 --- a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullApple.yaml +++ b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullApple.yaml @@ -9,3 +9,4 @@ scope: - foo - bar requested_claims: "#/definitions/OIDCClaims" +capture_last_tokens: true diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullGithub.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullGithub.yaml index 78940d859ea0..853440fb30f5 100644 --- a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullGithub.yaml +++ b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullGithub.yaml @@ -10,3 +10,4 @@ scope: - foo - bar requested_claims: "#/definitions/OIDCClaims" +capture_last_tokens: true diff --git a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullMicrosoft.yaml b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullMicrosoft.yaml index d17290f58bf9..bad31f11c376 100644 --- a/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullMicrosoft.yaml +++ b/test/schema/fixtures/config.schema.test.success/selfServiceOIDCProvider.fullMicrosoft.yaml @@ -11,3 +11,4 @@ scope: - bar microsoft_tenant: org requested_claims: "#/definitions/OIDCClaims" +capture_last_tokens: true