Skip to content

feat(oidc): add Google + Discord OIDC source plugins#4

Open
osindex wants to merge 7 commits into
linaproai:mainfrom
osindex:feat/oidc-plugins
Open

feat(oidc): add Google + Discord OIDC source plugins#4
osindex wants to merge 7 commits into
linaproai:mainfrom
osindex:feat/oidc-plugins

Conversation

@osindex

@osindex osindex commented May 27, 2026

Copy link
Copy Markdown

Adds two new source plugins that publish OAuth2 login entries on the workbench login page and finish the OAuth flow through the host's LoginByExternal seam (lina-core capability/contract.AuthService).

linapro-oidc-google + linapro-oidc-discord ship:

  • plugin.yaml: i18n.enabled with zh-CN default + en-US locale, source language English, menu entry under platform_only scope
  • manifest/i18n/{zh-CN,en-US}/{menu,plugin,error}.json plus apidoc/settings/settings.json for DTO meta translations
  • backend/api/settings/v1/settings.go: Get/Save DTOs; ClientSecret is intentionally NOT required because the GET masks the stored secret and PUT with empty value preserves it
  • backend/internal/service/config: typed Settings struct backed by host PluginSettingsService ("." rows in sys_config); no private SQL table
  • backend/internal/service/oauth: provider-specific OAuth2 client (authorize URL, code exchange, userinfo) with HMAC-signed state containing nonce + expires_at for CSRF + replay protection
  • backend/internal/controller/settings: GET masks secret, PUT preserves
  • backend/internal/controller/oauth: /api/v1/auth/ initiator + /callback handler. Callback validates state, exchanges code, fetches userinfo, hands off to authSvc.LoginByExternal, then either
    • SSO mode: enableBackendRedirect + state matches a backendRedirects rule => 302 to rule URL with tokens in query
    • SPA mode (everything else): 302 to /oauth-handoff with redirect target = defaultBackendRedirect (default /dashboard) Errors classified via bizerr RuntimeCode so the SPA handoff page shows a precise error (AUTH_EXTERNAL_USER_NOT_PROVISIONED, etc).
  • backend/internal/service/provider: authprovider.Provider impl reading live settings on each LoginEntry call so admin toggles take effect without restarting the plugin
  • backend/plugin.go: pluginhost.NewSourcePlugin + auth route binding + authprovider.RegisterProvider with the typed settings service
  • frontend/pages/-settings.vue: admin settings page using () for the instructions card (host locales/langs/plugins.json), read-only Redirect URI with copy, per-rule state URL copy
  • frontend/pages/google-login.vue: simple inline login button page
  • frontend/constants.ts: callback / login entry / console URL constants

go.mod + go.sum: add the two plugin modules + matching replace stanzas so the parent module resolves the new plugin source.

lina-plugins.go: blank import both plugins so plugin init() runs in the compiled host binary.

Verified locally:

  • google plugin: rtk go build ./... -> Success
  • discord plugin: rtk go build ./... -> Success
  • google plugin tests: 17 passed in 8 packages (state signing, settings config helpers, DTO validation, callback error classification)
  • discord plugin tests: 17 passed in 8 packages (same set)

osindex and others added 7 commits May 27, 2026 21:15
Adds two new source plugins that publish OAuth2 login entries on the
workbench login page and finish the OAuth flow through the host's
LoginByExternal seam (lina-core capability/contract.AuthService).

linapro-oidc-google + linapro-oidc-discord ship:
- plugin.yaml: i18n.enabled with zh-CN default + en-US locale, source
  language English, menu entry under platform_only scope
- manifest/i18n/{zh-CN,en-US}/{menu,plugin,error}.json plus
  apidoc/settings/settings.json for DTO meta translations
- backend/api/settings/v1/settings.go: Get/Save DTOs; ClientSecret is
  intentionally NOT required because the GET masks the stored secret
  and PUT with empty value preserves it
- backend/internal/service/config: typed Settings struct backed by host
  PluginSettingsService ("<pluginID>.<key>" rows in sys_config); no
  private SQL table
- backend/internal/service/oauth: provider-specific OAuth2 client
  (authorize URL, code exchange, userinfo) with HMAC-signed state
  containing nonce + expires_at for CSRF + replay protection
- backend/internal/controller/settings: GET masks secret, PUT preserves
- backend/internal/controller/oauth: /api/v1/auth/<provider> initiator +
  /callback handler. Callback validates state, exchanges code, fetches
  userinfo, hands off to authSvc.LoginByExternal, then either
    - SSO mode: enableBackendRedirect + state matches a backendRedirects
      rule => 302 to rule URL with tokens in query
    - SPA mode (everything else): 302 to /oauth-handoff with redirect
      target = defaultBackendRedirect (default /dashboard)
  Errors classified via bizerr RuntimeCode so the SPA handoff page
  shows a precise error (AUTH_EXTERNAL_USER_NOT_PROVISIONED, etc).
- backend/internal/service/provider: authprovider.Provider impl reading
  live settings on each LoginEntry call so admin toggles take effect
  without restarting the plugin
- backend/plugin.go: pluginhost.NewSourcePlugin + auth route binding +
  authprovider.RegisterProvider with the typed settings service
- frontend/pages/<provider>-settings.vue: admin settings page using
  () for the instructions card (host locales/langs/plugins.json),
  read-only Redirect URI with copy, per-rule state URL copy
- frontend/pages/google-login.vue: simple inline login button page
- frontend/constants.ts: callback / login entry / console URL constants

go.mod + go.sum: add the two plugin modules + matching replace stanzas
so the parent module resolves the new plugin source.

lina-plugins.go: blank import both plugins so plugin init() runs in the
compiled host binary.

Verified locally:
- google plugin: rtk go build ./... -> Success
- discord plugin: rtk go build ./... -> Success
- google plugin tests: 17 passed in 8 packages (state signing, settings
  config helpers, DTO validation, callback error classification)
- discord plugin tests: 17 passed in 8 packages (same set)
…der parent

Adds parent_key: auth-provider to each OIDC plugin's settings menu in
plugin.yaml so the host plugin lifecycle menu sync inserts the page
under the new host-level 'auth-provider' directory instead of as a
loose top-level menu.

The 'auth-provider' parent menu is seeded by the host at
apps/lina-core/manifest/sql/013-auth-provider-management.sql and
localized via the host menu i18n resources, so both plugins coexist
under a single 授权管理 (zh-CN) / Authentication Providers (en-US)
navigation entry even when only one of them is installed.
auth-provider is now materialized and removed by the host on plugin demand. Plugins only reference parent_key; they no longer define the parent catalog's name/icon/sort. README host-boundary section updated accordingly.
…EADME.zh-CN

Keep the bilingual README in sync: the Chinese mirror now matches the English host-boundary section describing the host-owned on-demand auth-provider catalog.
Add linapro-oidc-google and linapro-oidc-discord (source, platform_only, global) to the bilingual plugin inventory tables.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
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.

1 participant