Skip to content

feat: allow using identity external_id as oauth2 subject#4529

Open
Micaso wants to merge 2 commits intoory:masterfrom
Micaso:feat/external-id-as-subject-for-hydra
Open

feat: allow using identity external_id as oauth2 subject#4529
Micaso wants to merge 2 commits intoory:masterfrom
Micaso:feat/external-id-as-subject-for-hydra

Conversation

@Micaso
Copy link

@Micaso Micaso commented Jan 23, 2026

Description

This PR introduces the ability to use an identity's external_id as the OpenID Connect subject (sub claim) when Ory Kratos acts as the identity provider for Ory Hydra.

Previously, Kratos always passed the internal UUID as the subject. For users migrating from legacy systems or integrating with third-party services that rely on specific string identifiers, this new configuration option allows for seamless identity mapping without requiring a custom consent provider middleware.

The logic implements a safe fallback: if the configuration is enabled but an identity does not have an external_id set, it will continue to use the Kratos Identity UUID to prevent flow breakage.

Related issue(s)

Fixes #4528

Checklist

  • I have read the contributing guidelines.
  • I have referenced an issue containing the design document if my change introduces a new feature.
  • I am following the contributing code guidelines.
  • I have read the security policy.
  • I confirm that this pull request does not address a security vulnerability.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added or changed the documentation.

Further Comments

Solution Choice

I implemented this via a new configuration toggle oauth2_provider.use_external_id. This maintains backward compatibility by defaulting to false.

The subject selection logic in hydra/hydra.go follows this priority:

  1. If oauth2_provider.use_external_id is false: Use Identity.ID.
  2. If oauth2_provider.use_external_id is true AND Identity.ExternalID is present: Use Identity.ExternalID.
  3. If oauth2_provider.use_external_id is true AND Identity.ExternalID is empty: Fallback to Identity.ID.

Alternatives Considered

Custom Integration Layer: One could bypass the native Kratos-Hydra integration and handle the acceptLoginRequest manually in a custom UI/backend. However, this requires users to write and maintain "glue code" for a very common architectural need. Providing this natively simplifies the Ory stack for enterprise migrations.

Testing

  • Updated driver/config/config_test.go to verify schema parsing.
  • Updated hydra/fake.go to capture and verify parameters.
  • Verified that the PostLoginHook correctly extracts the external_id from the identity.

This change adds a configuration option `oauth2_provider.use_external_id`.
When enabled, Kratos will pass the identity's `external_id` as the
subject (`sub`) to Ory Hydra during the OAuth2 login flow.

If the toggle is enabled but no `external_id` is present on the identity,
it falls back to the internal Identity ID (UUID) to ensure continuity.

Part of the effort to better integrate external identity mappings.

Closes ory#4528
@Micaso Micaso requested review from a team and aeneasr as code owners January 23, 2026 07:11
@Micaso
Copy link
Author

Micaso commented Jan 26, 2026

if I run make format it would only format 2 files which are not effected by me:

oryx/httpx/resilient_client.go
oryx/tlsx/termination.go

Also I am quite unsure if I could resolve the issues from the Docker Image Scanners

Copy link
Member

@jonas-jonas jonas-jonas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks for your work here! I have a few notes.

headers:
Authorization: Basic
override_return_to: true
use_external_id: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the same pattern for the tokenizer, but there the setting is

subject_source: external_id # or id (default)

Could you adjust the code here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi there! Thx for your review and apologies for the delay in getting back to this—I’ve been a bit tied up lately. I changed it like you requested

hydra/hydra.go Outdated
Comment on lines +98 to +100
if h.d.Config().OAuth2ProviderUseExternalID(ctx) && params.ExternalID != "" {
subject = params.ExternalID
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the tokenizer we return an error if the identity's external_id is unset, and the subject_source is set to external_id.

I think that would be appropriate here, too, as it's otherwise difficult to figure out which ID was used.

Alternatively, we could probably add another claim, that describes the subject_source to the token, WDYT?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes total sense. I agree that consistency with the tokenizer is the best path forward here to avoid confusion over which ID is being used.

I’ve updated the code to return an error if external_id is unset while subject_source is set to external_id. This keeps the behavior predictable across the codebase. Thanks for pointing that out

Replace `use_external_id` boolean config with `subject_source` enum to
match the existing tokenizer pattern. The new config accepts:
- "id" (default): Use identity ID as OAuth2 subject
- "external_id": Use identity's external_id as OAuth2 subject

Returns an error when `subject_source` is set to "external_id" but the
identity's external_id is unset, ensuring predictable behavior and
making it easier to identify which ID was used.

This aligns the OAuth2 provider configuration with the session
tokenizer implementation for consistency across the codebase.

Closes ory#4528
rememberFor := int64(h.d.Config().SessionLifespan(ctx) / time.Second)

alr := hydraclientgo.NewAcceptOAuth2LoginRequest(params.IdentityID)
subject := params.IdentityID

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

This definition of subject is never used.
@Micaso Micaso requested a review from jonas-jonas February 23, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: allow using external_id as the subject in OAuth2 login flows

2 participants