skillhub/server/skillhub-app
XiaoSeS 96f244b416 feat(auth): add DingTalk as a public login provider
Adds DingTalk (钉钉) as a public sign-in option: it authenticates a SkillHub
platform account and nothing more. No Organization membership, no directory
sync, no Namespace grants.

DingTalk deviates from standard OAuth at all three stages, one strategy each:

- authorize: its endpoint wants scope=openid, but declaring that scope in
  configuration makes Spring treat the registration as OIDC and attach a
  nonce, which DingTalk rejects. The scope is added by
  DingTalkAuthorizationRequestCustomizer instead, keeping this a plain OAuth2
  client. A test asserts the scope is present and the nonce is not.
- token: credentials go in a JSON body rather than a form, handled by
  DingTalkTokenResponseClient.
- userinfo: the token travels in x-acs-dingtalk-access-token rather than
  Authorization: Bearer.

Subject and email semantics, which decide whether a login can reach an
existing account:

- unionId is the only accepted subject. DingTalk also returns openId and
  userId, but they must not act as fallbacks: openId is scoped per app and
  userId per organization, so a login falling back to either would bind a
  different identity than a later login carrying unionId, splitting one
  person across two platform accounts.
- A blank or missing unionId fails the login.
- emailVerified is always false. DingTalk returns the email an organization
  admin recorded without attesting the user controls it.

The userinfo service only fetches attributes; account matching, provisioning
and session creation stay with the unified identity core. The reference
implementation called OAuthLoginFlowService.authenticate() from inside
loadUser, which decided the account before the core's gate ran.

Operational bounds match the Feishu adapter: connect and read timeouts, a
64 KB response cap, error descriptions and logs carrying only the exception
class or provider error code, and no logging in the claims extractor.
Unused PII is dropped rather than carried into the principal -- notably
mobile and stateCode.

Adds ProviderStrategyWiringTest, which loads the real application context.
The unit tests call package-visible constructors and so cannot catch Spring
wiring faults; a component with two constructors and no @Autowired marker
unit-tests green and then fails at startup. That happened during this work.

Adapted from the implementation in #467 by @konglong87, re-extracted onto
current main with the subject, structure and bounds changes above.

Part of R1-A2 (public Provider adapters) per
openspec/changes/enterprise-identity-platform/rollout-plan.md.

Co-authored-by: konglong87 <konglong87@users.noreply.github.com>
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
2026-09-21 15:15:41 +08:00
..
src feat(auth): add DingTalk as a public login provider 2026-09-21 15:15:41 +08:00
pom.xml feat(review): add skill comments and user feedback (#793) 2026-09-02 11:15:52 +08:00