diff --git a/.gitignore b/.gitignore
index d91c2fb7d111..f792761b2b71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,8 @@ heap_profiler/
 goroutine_dump/
 inflight_trace_dump/
 
+contrib/quickstart/kratos/oidc
+
 e2e/*.log
 e2e/kratos.*.yml
 e2e/proxy.json
diff --git a/driver/config/config_test.go b/driver/config/config_test.go
index b2047e2fc433..2a8d57e7bebb 100644
--- a/driver/config/config_test.go
+++ b/driver/config/config_test.go
@@ -15,7 +15,6 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
-	"sync"
 	"testing"
 	"time"
 
@@ -1043,35 +1042,23 @@ func TestIdentitySchemaValidation(t *testing.T) {
 				t.Cleanup(cancel)
 
 				_, hook, writeSchema := testWatch(t, ctx, &cobra.Command{}, identity)
-
-				var wg sync.WaitGroup
-				wg.Add(1)
-				go func() {
-					defer wg.Done()
-					// Change the identity config to an invalid file
-					writeSchema(invalidIdentity.Identity.Schemas)
-				}()
+				writeSchema(invalidIdentity.Identity.Schemas)
 
 				// There are a bunch of log messages beeing logged. We are looking for a specific one.
-				timeout := time.After(time.Millisecond * 500)
-				success := false
-				for !success {
+				for {
 					for _, v := range hook.AllEntries() {
 						s, err := v.String()
 						require.NoError(t, err)
-						success = success || strings.Contains(s, "The changed identity schema configuration is invalid and could not be loaded.")
+						if strings.Contains(s, "The changed identity schema configuration is invalid and could not be loaded.") {
+							return
+						}
 					}
-
 					select {
 					case <-ctx.Done():
 						t.Fatal("the test could not complete as the context timed out before the file watcher updated")
-					case <-timeout:
-						t.Fatal("Expected log line was not encountered within specified timeout")
 					default: // nothing
 					}
 				}
-
-				wg.Wait()
 			})
 		}
 	})
diff --git a/embedx/config.schema.json b/embedx/config.schema.json
index c7a7be169224..473f0bf514fd 100644
--- a/embedx/config.schema.json
+++ b/embedx/config.schema.json
@@ -436,6 +436,7 @@
             "dingtalk",
             "patreon",
             "linkedin",
+            "linkedin_v2",
             "lark",
             "x"
           ],
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
new file mode 100644
index 000000000000..95bff506986a
--- /dev/null
+++ b/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json
@@ -0,0 +1 @@
+"{\"providers\":[{\"initial_id_token\":\"id_token0\",\"initial_access_token\":\"access_token0\",\"initial_refresh_token\":\"refresh_token0\",\"subject\":\"foo\",\"provider\":\"bar\",\"organization\":\"\"},{\"initial_id_token\":\"id_token1\",\"initial_access_token\":\"access_token1\",\"initial_refresh_token\":\"refresh_token1\",\"subject\":\"baz\",\"provider\":\"zab\",\"organization\":\"\"}]}"
diff --git a/identity/handler.go b/identity/handler.go
index 0343567a0ac7..8622a2e76d8e 100644
--- a/identity/handler.go
+++ b/identity/handler.go
@@ -251,7 +251,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para
 	}
 
 	// Identities using the marshaler for including metadata_admin
-	isam := make([]WithCredentialsMetadataAndAdminMetadataInJSON, len(is))
+	isam := make([]WithCredentialsAndAdminMetadataInJSON, len(is))
 	for i, identity := range is {
 		emit, err := identity.WithDeclassifiedCredentials(r.Context(), h.r, params.DeclassifyCredentials)
 		if err != nil {
@@ -259,7 +259,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para
 			return
 		}
 
-		isam[i] = WithCredentialsMetadataAndAdminMetadataInJSON(*emit)
+		isam[i] = WithCredentialsAndAdminMetadataInJSON(*emit)
 	}
 
 	h.r.Writer().Write(w, r, isam)
diff --git a/identity/handler_test.go b/identity/handler_test.go
index 9444da92a0e8..bfdf1a86dfc3 100644
--- a/identity/handler_test.go
+++ b/identity/handler_test.go
@@ -1348,11 +1348,15 @@ func TestHandler(t *testing.T) {
 	})
 
 	t.Run("case=should list all identities with credentials", func(t *testing.T) {
-		res := get(t, adminTS, "/identities?include_credential=totp", http.StatusOK)
-		assert.True(t, res.Get("0.credentials").Exists(), "credentials config should be included: %s", res.Raw)
-		assert.True(t, res.Get("0.metadata_public").Exists(), "metadata_public config should be included: %s", res.Raw)
-		assert.True(t, res.Get("0.metadata_admin").Exists(), "metadata_admin config should be included: %s", res.Raw)
-		assert.EqualValues(t, "baz", res.Get(`#(traits.bar=="baz").traits.bar`).String(), "%s", res.Raw)
+		t.Run("include_credential=oidc should include OIDC credentials config", func(t *testing.T) {
+			res := get(t, adminTS, "/identities?include_credential=oidc&credentials_identifier=bar:foo.oidc@bar.com", http.StatusOK)
+			assert.True(t, res.Get("0.credentials.oidc.config").Exists(), "credentials config should be included: %s", res.Raw)
+			snapshotx.SnapshotT(t, res.Get("0.credentials.oidc.config").String())
+		})
+		t.Run("include_credential=totp should not include OIDC credentials config", func(t *testing.T) {
+			res := get(t, adminTS, "/identities?include_credential=totp&credentials_identifier=bar:foo.oidc@bar.com", http.StatusOK)
+			assert.False(t, res.Get("0.credentials.oidc.config").Exists(), "credentials config should be included: %s", res.Raw)
+		})
 	})
 
 	t.Run("case=should not be able to list all identities with credentials due to wrong credentials type", func(t *testing.T) {
diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum
index c966c8ddfd0d..6cc3f5911d11 100644
--- a/internal/client-go/go.sum
+++ b/internal/client-go/go.sum
@@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go
index cb22ebb6b847..30ea305a22ed 100644
--- a/selfservice/strategy/oidc/provider.go
+++ b/selfservice/strategy/oidc/provider.go
@@ -5,8 +5,10 @@ package oidc
 
 import (
 	"context"
+	"encoding/json"
 	"net/http"
 	"net/url"
+	"strings"
 
 	"github.com/dghubble/oauth1"
 	"github.com/pkg/errors"
@@ -68,7 +70,7 @@ type Claims struct {
 	Gender              string                 `json:"gender,omitempty"`
 	Birthdate           string                 `json:"birthdate,omitempty"`
 	Zoneinfo            string                 `json:"zoneinfo,omitempty"`
-	Locale              string                 `json:"locale,omitempty"`
+	Locale              Locale                 `json:"locale,omitempty"`
 	PhoneNumber         string                 `json:"phone_number,omitempty"`
 	PhoneNumberVerified bool                   `json:"phone_number_verified,omitempty"`
 	UpdatedAt           int64                  `json:"updated_at,omitempty"`
@@ -79,6 +81,29 @@ type Claims struct {
 	RawClaims           map[string]interface{} `json:"raw_claims,omitempty"`
 }
 
+type Locale string
+
+func (l *Locale) UnmarshalJSON(data []byte) error {
+	var linkedInLocale struct {
+		Language string `json:"language"`
+		Country  string `json:"country"`
+	}
+	if err := json.Unmarshal(data, &linkedInLocale); err == nil {
+		switch {
+		case linkedInLocale.Language == "":
+			*l = Locale(linkedInLocale.Country)
+		case linkedInLocale.Country == "":
+			*l = Locale(linkedInLocale.Language)
+		default:
+			*l = Locale(strings.Join([]string{linkedInLocale.Language, linkedInLocale.Country}, "-"))
+		}
+
+		return nil
+	}
+
+	return json.Unmarshal(data, (*string)(l))
+}
+
 // Validate checks if the claims are valid.
 func (c *Claims) Validate() error {
 	if c.Subject == "" {
diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go
index 0e579906a94f..4eac8be4f7db 100644
--- a/selfservice/strategy/oidc/provider_config.go
+++ b/selfservice/strategy/oidc/provider_config.go
@@ -141,26 +141,27 @@ type ConfigurationCollection struct {
 // If you add a provider here, please also add a test to
 // provider_private_net_test.go
 var supportedProviders = map[string]func(config *Configuration, reg Dependencies) Provider{
-	"generic":    NewProviderGenericOIDC,
-	"google":     NewProviderGoogle,
-	"github":     NewProviderGitHub,
-	"github-app": NewProviderGitHubApp,
-	"gitlab":     NewProviderGitLab,
-	"microsoft":  NewProviderMicrosoft,
-	"discord":    NewProviderDiscord,
-	"slack":      NewProviderSlack,
-	"facebook":   NewProviderFacebook,
-	"auth0":      NewProviderAuth0,
-	"vk":         NewProviderVK,
-	"yandex":     NewProviderYandex,
-	"apple":      NewProviderApple,
-	"spotify":    NewProviderSpotify,
-	"netid":      NewProviderNetID,
-	"dingtalk":   NewProviderDingTalk,
-	"linkedin":   NewProviderLinkedIn,
-	"patreon":    NewProviderPatreon,
-	"lark":       NewProviderLark,
-	"x":          NewProviderX,
+	"generic":     NewProviderGenericOIDC,
+	"google":      NewProviderGoogle,
+	"github":      NewProviderGitHub,
+	"github-app":  NewProviderGitHubApp,
+	"gitlab":      NewProviderGitLab,
+	"microsoft":   NewProviderMicrosoft,
+	"discord":     NewProviderDiscord,
+	"slack":       NewProviderSlack,
+	"facebook":    NewProviderFacebook,
+	"auth0":       NewProviderAuth0,
+	"vk":          NewProviderVK,
+	"yandex":      NewProviderYandex,
+	"apple":       NewProviderApple,
+	"spotify":     NewProviderSpotify,
+	"netid":       NewProviderNetID,
+	"dingtalk":    NewProviderDingTalk,
+	"linkedin":    NewProviderLinkedIn,
+	"linkedin_v2": NewProviderLinkedInV2,
+	"patreon":     NewProviderPatreon,
+	"lark":        NewProviderLark,
+	"x":           NewProviderX,
 }
 
 func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) {
diff --git a/selfservice/strategy/oidc/provider_discord.go b/selfservice/strategy/oidc/provider_discord.go
index 181e7df6d322..99bea24d5770 100644
--- a/selfservice/strategy/oidc/provider_discord.go
+++ b/selfservice/strategy/oidc/provider_discord.go
@@ -93,7 +93,7 @@ func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, qu
 		Picture:           user.AvatarURL(""),
 		Email:             user.Email,
 		EmailVerified:     x.ConvertibleBoolean(user.Verified),
-		Locale:            user.Locale,
+		Locale:            Locale(user.Locale),
 	}
 
 	return claims, nil
diff --git a/selfservice/strategy/oidc/provider_linkedin_v2.go b/selfservice/strategy/oidc/provider_linkedin_v2.go
new file mode 100644
index 000000000000..7ce40239ef46
--- /dev/null
+++ b/selfservice/strategy/oidc/provider_linkedin_v2.go
@@ -0,0 +1,47 @@
+// Copyright © 2023 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package oidc
+
+import (
+	"context"
+	"net/url"
+
+	gooidc "github.com/coreos/go-oidc/v3/oidc"
+	"golang.org/x/oauth2"
+)
+
+type ProviderLinkedInV2 struct {
+	*ProviderGenericOIDC
+}
+
+func NewProviderLinkedInV2(
+	config *Configuration,
+	reg Dependencies,
+) Provider {
+	config.ClaimsSource = ClaimsSourceUserInfo
+	config.IssuerURL = "https://www.linkedin.com/oauth"
+
+	return &ProviderLinkedInV2{
+		ProviderGenericOIDC: &ProviderGenericOIDC{
+			config: config,
+			reg:    reg,
+		},
+	}
+}
+
+func (l *ProviderLinkedInV2) wrapCtx(ctx context.Context) context.Context {
+	// We need to overwrite the issuer here because the discovery URL is under
+	// `https://www.linkedin.com/oauth/.well-known/openid-configuration`, wherease
+	// the issuer is `https://www.linkedin.com` (without the `/oauth`). This is
+	// not conformant according to the OIDC spec, but needed for LinkedIn.
+	return gooidc.InsecureIssuerURLContext(ctx, "https://www.linkedin.com")
+}
+
+func (l *ProviderLinkedInV2) OAuth2(ctx context.Context) (*oauth2.Config, error) {
+	return l.ProviderGenericOIDC.OAuth2(l.wrapCtx(ctx))
+}
+
+func (l *ProviderLinkedInV2) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
+	return l.ProviderGenericOIDC.Claims(l.wrapCtx(ctx), exchange, query)
+}
diff --git a/selfservice/strategy/oidc/provider_linkedin_v2_test.go b/selfservice/strategy/oidc/provider_linkedin_v2_test.go
new file mode 100644
index 000000000000..c36e44473fa7
--- /dev/null
+++ b/selfservice/strategy/oidc/provider_linkedin_v2_test.go
@@ -0,0 +1,34 @@
+// Copyright © 2023 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package oidc_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ory/kratos/internal"
+	"github.com/ory/kratos/selfservice/strategy/oidc"
+)
+
+func TestProviderLinkedInV2_Discovery(t *testing.T) {
+	_, reg := internal.NewVeryFastRegistryWithoutDB(t)
+
+	p := oidc.NewProviderLinkedInV2(&oidc.Configuration{
+		Provider:        "linkedin_v2",
+		ID:              "valid",
+		ClientID:        "client",
+		ClientSecret:    "secret",
+		Mapper:          "file://./stub/hydra.schema.json",
+		RequestedClaims: nil,
+		Scope:           []string{"email", "profile", "offline_access"},
+	}, reg)
+
+	c, err := p.(oidc.OAuth2Provider).OAuth2(context.Background())
+	require.NoError(t, err)
+	assert.Contains(t, c.Scopes, "openid")
+	assert.Equal(t, "https://www.linkedin.com/oauth/v2/accessToken", c.Endpoint.TokenURL)
+}
diff --git a/selfservice/strategy/oidc/provider_private_net_test.go b/selfservice/strategy/oidc/provider_private_net_test.go
index e656ee0462bb..0505a3e19626 100644
--- a/selfservice/strategy/oidc/provider_private_net_test.go
+++ b/selfservice/strategy/oidc/provider_private_net_test.go
@@ -73,6 +73,7 @@ func TestProviderPrivateIP(t *testing.T) {
 		// GitHub uses a fixed token URL and does not use the issuer.
 		// GitHub App uses a fixed token URL and does not use the issuer.
 		// GitHub App uses a fixed token URL and does not use the issuer.
+		// LinkedInV2 uses a fixed token URL and does not use the issuer.
 
 		{p: gitlab, c: &oidc.Configuration{IssuerURL: "http://127.0.0.2/"}, e: "is not a permitted destination"},
 		// The TokenURL is fixed in GitLab to {issuer_url}/token. Since the issuer is called first, any local token fails also.
diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go
index 7c0de7c55138..a5733d2e95f8 100644
--- a/selfservice/strategy/oidc/provider_test.go
+++ b/selfservice/strategy/oidc/provider_test.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -42,7 +43,7 @@ func RegisterTestProvider(id string) func() {
 
 var _ IDTokenVerifier = new(TestProvider)
 
-func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error) {
+func (t *TestProvider) Verify(_ context.Context, token string) (*Claims, error) {
 	if token == "error" {
 		return nil, fmt.Errorf("stub error")
 	}
@@ -52,3 +53,56 @@ func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error
 	}
 	return &c, nil
 }
+
+func TestLocale(t *testing.T) {
+	// test json unmarshal
+	for _, tc := range []struct {
+		name      string
+		json      string
+		expected  string
+		assertErr assert.ErrorAssertionFunc
+	}{{
+		name:     "empty",
+		json:     `{}`,
+		expected: "",
+	}, {
+		name:     "empty string locale",
+		json:     `{"locale":""}`,
+		expected: "",
+	}, {
+		name:      "invalid string locale",
+		json:      `{"locale":"""}`,
+		assertErr: assert.Error,
+	}, {
+		name:     "string locale",
+		json:     `{"locale":"en-US"}`,
+		expected: "en-US",
+	}, {
+		name:     "linkedin locale",
+		json:     `{"locale":{"country":"US","language":"en","ignore":"me"}}`,
+		expected: "en-US",
+	}, {
+		name:     "missing country linkedin locale",
+		json:     `{"locale":{"language":"en"}}`,
+		expected: "en",
+	}, {
+		name:     "missing language linkedin locale",
+		json:     `{"locale":{"country":"US"}}`,
+		expected: "US",
+	}, {
+		name:     "invalid linkedin locale",
+		json:     `{"locale":{"invalid":"me"}}`,
+		expected: "",
+	}} {
+		t.Run(tc.name, func(t *testing.T) {
+			var c Claims
+			err := json.Unmarshal([]byte(tc.json), &c)
+			if tc.assertErr != nil {
+				tc.assertErr(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.EqualValues(t, tc.expected, c.Locale)
+		})
+	}
+}
diff --git a/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts b/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts
index 95fafafa0f30..78426b7cd9f4 100644
--- a/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts
+++ b/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts
@@ -4,7 +4,7 @@
 import { gen } from "../../../helpers"
 import { routes as express } from "../../../helpers/express"
 import { routes as react } from "../../../helpers/react"
-import { testRegistrationWebhook } from "../../../helpers/webhook"
+import { testFlowWebhook } from "../../../helpers/webhook"
 
 const signup = (registration: string, app: string, email = gen.email()) => {
   cy.visit(registration)
@@ -158,8 +158,12 @@ context("Passkey registration", () => {
       })
 
       it("should pass transient_payload to webhook", () => {
-        testRegistrationWebhook(
-          (hooks) => cy.setupHooks("registration", "after", "passkey", hooks),
+        testFlowWebhook(
+          (hooks) =>
+            cy.setupHooks("registration", "after", "passkey", [
+              ...hooks,
+              { hook: "session" },
+            ]),
           () => {
             signup(registration, app)
           },
diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts
index 33b8bafe8735..cd4cf069c556 100644
--- a/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts
+++ b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts
@@ -4,7 +4,7 @@
 import { appPrefix, gen, website } from "../../../../helpers"
 import { routes as express } from "../../../../helpers/express"
 import { routes as react } from "../../../../helpers/react"
-import { testRegistrationWebhook } from "../../../../helpers/webhook"
+import { testFlowWebhook } from "../../../../helpers/webhook"
 
 context("Social Sign Up Successes", () => {
   ;[
@@ -104,8 +104,12 @@ context("Social Sign Up Successes", () => {
       })
 
       it("should pass transient_payload to webhook", () => {
-        testRegistrationWebhook(
-          (hooks) => cy.setupHooks("registration", "after", "oidc", hooks),
+        testFlowWebhook(
+          (hooks) =>
+            cy.setupHooks("registration", "after", "oidc", [
+              ...hooks,
+              { hook: "session" },
+            ]),
           () => {
             const email = gen.email()
             cy.registerOidc({