Skip to content

feat(oidc): Initial OpenID Connect Support#1091

Merged
looplj merged 66 commits into
looplj:unstablefrom
LazuliKao:feat/oidc
Apr 30, 2026
Merged

feat(oidc): Initial OpenID Connect Support#1091
looplj merged 66 commits into
looplj:unstablefrom
LazuliKao:feat/oidc

Conversation

@LazuliKao

@LazuliKao LazuliKao commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Work in progress, feel free to point out any suggestion.
单点登录初步支持,请大家建言献策。目前在PocketID测试通过。
Related issue: #844

功能检查单:

  • 基本跑通
  • OIDC 通过issuer配置自动发现
  • 通过OIDC提供的邮箱完成账号自动绑定 (配置可开关)
  • OIDC首次登录自动创建账号(配置可开关,关闭则用户只能在已有账号的密码页面绑定OIDC才能用OIDC登录)
  • 实现 仅允许OIDC登录,并提供配置开关
  • OIDC 不通过issuer,手动配置每个接口地址
  • 测试GitHub OAuth兼容性的
  • OIDC配置,目前配置文件支持,环境变量传的是整个JSON,管理员UI(图形界面)无配置功能。
  • PKCE 通过测试
  • OIDC角色自动映射(groups/roles claim)(尚未完整测试)
  • ...
图骗

Config Example:

# OIDC configuration
oidc:
  providers:
    - name: google
      display_name: Google
      issuer_url: https://accounts.google.com
      client_id: your-client-id
      client_secret: your-client-secret
      extra_scopes: [openid, profile, email]
      jit_enabled: true
      auto_link_by_email: true
      require_email_verified: true
      enable_pkce: true
      sync_user_info: true
      icon_url: https://example.com/google.svg
      button_color: "#ffffff"

附带实现了一些必要功能

密码修改

  • 当用户使用OIDC登录,该页面显示设置初始密码,当用户设置过密码,在此页面可以修改密码
image
  • 用户可以自由 link 和 unlink 一个或者多个可用的OIDC提供商,当用户在未设置密码的情况下尝试取消关联所有OIDC提供商会被阻止
image

OIDC 认证流程

本文档说明当前这个 PR 中 OIDC 实现的前后端协作方式,主要用于帮助 reviewer 对照 internal/server/biz/oidc.gointernal/server/api/oidc.go 以及前端 frontend/src/features/auth 相关代码理解整体设计。

概览

AxonHub 当前采用的是“后端主导”的 OIDC 登录流程:

  1. 前端先向后端请求授权地址。
  2. 用户被重定向到身份提供商(IdP)。
  3. IdP 回调 AxonHub 后端。
  4. 后端验证 OIDC 返回结果并解析 AxonHub 用户。
  5. 后端生成一个短时有效、一次性的 exchange code。
  6. 后端再把浏览器重定向到前端回调页。
  7. 前端用这个 code 向后端换取 AxonHub 自己的 JWT。

这意味着前端不会拿到原始的 IdP id_token,也不会在浏览器里自己做 OIDC token 验证。

端到端流程

flowchart TD
    U[用户在 /sign-in 点击某个 OIDC Provider] --> F1[前端调用<br/>GET /oauth/oidc/authorize/:provider]
    F1 --> B1[后端构造授权 URL<br/>并缓存 state/PKCE verifier]
    B1 --> IDP[浏览器跳转到 IdP]
    IDP --> B2[IdP 回调至回调接口<br/>GET /oauth/oidc/callback 或 /oauth/oidc/callback/:provider]
    B2 --> B3[后端校验 state / PKCE<br/>并交换授权码]
    B3 --> B4[后端校验 id_token<br/>并解析 claims]
    B4 --> B5{解析 AxonHub 用户}
    B5 -->|已有 issuer+subject 绑定| B6[复用已绑定用户]
    B5 -->|命中已验证邮箱| B7[把身份绑定到已有用户]
    B5 -->|无用户且 JIT 开启| B8[创建 AxonHub 用户<br/>并创建 OIDCIdentity]
    B6 --> B9[生成 5 分钟有效、一次性的<br/>exchange code]
    B7 --> B9
    B8 --> B9
    B9 --> F2[后端重定向到<br/>/oauth/oidc/idp-callback?code=...]
    F2 --> F3[前端回调页调用<br/>POST /oauth/oidc/exchange]
    F3 --> B10[后端校验 exchange code<br/>并签发 AxonHub JWT]
    B10 --> F4[前端写入 Zustand + localStorage<br/>并跳转进入应用]
Loading

后端流程

公开路由

OIDC HTTP 路由注册在 internal/server/routes.gointernal/server/api/oidc.go 中,统一挂在 /oauth/oidc 下:

  • GET /oauth/oidc/providers
  • GET /oauth/oidc/authorize/:provider
  • GET /oauth/oidc/callback
  • GET /oauth/oidc/callback/:provider
  • POST /oauth/oidc/exchange

此外,还有一个给已登录用户使用的受保护绑定入口:

  • GET /admin/oidc/link/:provider

Provider 初始化

Provider 配置定义在 conf/conf.gooidc.providers 下,运行时逻辑在 internal/server/biz/oidc.go

服务启动时,NewOIDCService 会基于配置里的 issuer_url 调用 oidc.NewProvider(...) 尝试初始化每个 provider,并为其构造 OAuth2 配置。后端给 IdP 提供的回调地址是:

  • 仅配置一个 provider 时:/oauth/oidc/callback
  • 配置多个 provider 时:/oauth/oidc/callback/:provider

这个回调地址之后会再拼接上当前请求域名,或者 server.public_url,变成绝对 URL 发给 IdP。

第一步:前端请求授权地址

前端通过 GET /oauth/oidc/authorize/:provider 发起登录。

OIDCService.GetAuthorizeURL(...) 会:

  1. 按名称解析 provider。
  2. 如果启动时初始化失败,则按需懒加载重试。
  3. 生成随机 state
  4. 如果 enable_pkce 为真,则生成并缓存 PKCE verifier。
  5. 返回最终的 IdP 授权地址。

如果某个 provider 刚失败过,服务端会对该 provider 应用 60 秒的重试退避,避免频繁重建。

第二步:IdP 回调到后端

用户在 IdP 完成登录后,会被重定向回:

  • GET /oauth/oidc/callback
  • GET /oauth/oidc/callback/:provider

OIDCHandlers.Callback(...) 会提取 codestate,然后调用 OIDCService.Callback(...)

OIDCService.Callback(...) 内,后端会:

  1. 如果启用了 PKCE,则从缓存中取回 verifier。
  2. 通过 oauth2.Config.Exchange(...) 用授权码向 IdP 换 token。
  3. 从 OAuth2 响应中提取原始 id_token
  4. 使用 go-oidc 校验 id_token
  5. 解析 emailemail_verifiednamegiven_namefamily_namepicture 等 claims。

第三步:解析或创建 AxonHub 用户

核心用户解析逻辑在 OIDCService.resolveUser(...) 中,当前顺序如下:

  1. 已有 OIDC 身份:按 (issuer, subject) 查找,命中后直接复用该用户。
  2. 已验证邮箱匹配:如果还没有 OIDCIdentity,但 IdP 返回了已验证邮箱,则按邮箱查找 AxonHub 用户,并把 OIDC 身份绑定到该用户。
  3. JIT 自动建号:如果仍然找不到用户,且 jit_enabled 开启,则创建 AxonHub 用户,再创建对应的 OIDCIdentity。

额外行为:

  • 如果 sync_user_info 开启,登录或绑定时会用 OIDC claims 同步 AxonHub 用户的姓名与头像。
  • 如果 require_email_verified 开启,则新用户 JIT 创建要求 email_verified=true
  • 新创建的 OIDC-only 用户密码会被写成特殊占位值 !OIDC_SSO_ONLY!,从而在 biz/auth.go 中阻止密码登录。
  • 如果最终解析出的用户状态为 deactivated,则登录会被拒绝。

第四步:生成短时 exchange code

当后端已经确认 AxonHub 用户之后,它不会直接在 IdP 回调阶段签发最终的 dashboard JWT。

相反,OIDCService.Callback(...) 会:

  1. 生成随机 exchange code。
  2. userID 缓存在 oidc_exchange:<code> 下。
  3. 设置 5 分钟过期时间。

随后回调 handler 会把浏览器重定向到前端路由:

/oauth/oidc/idp-callback?code=<exchange_code>

这样做的目的,是把 OIDC 协议细节留在后端,同时让前端仍然按照现有“拿 AxonHub 自己的 JWT 建立会话”的模型完成登录。

第五步:前端用 code 换 AxonHub JWT

前端回调页会调用 POST /oauth/oidc/exchange,把 exchange code 发回后端。

OIDCService.ExchangeCode(...) 会:

  1. 从缓存中读取用户 ID。
  2. 拒绝过期或不存在的 exchange code。
  3. 立即删除该 code,确保它只能使用一次。
  4. 加载对应用户。

随后 OIDCHandlers.Exchange(...) 会调用 AuthService.GenerateJWTToken(...) 签发 AxonHub JWT,特征如下:

  • 算法:HS256
  • 有效期:7 天
  • claims:user_idexp

返回体格式如下:

{
  "data": {
    "token": "<axonhub-jwt>",
    "user": { "...": "..." }
  }
}

前端流程

登录页

前端 OIDC 登录相关代码主要在:

  • frontend/src/features/auth/data/auth.ts
  • frontend/src/routes/oauth/oidc/idp-callback.tsx
  • frontend/src/lib/api-client.ts
  • frontend/src/stores/authStore.ts

OIDC 登录按钮流程如下:

  1. useOIDCProviders()GET /oauth/oidc/providers 获取可用 provider。
  2. useOIDCAuthorize() 调用 GET /oauth/oidc/authorize/:provider
  3. 成功后直接把浏览器跳转到后端返回的 IdP 授权地址。

前端回调路由

后端回调完成后,浏览器会进入公开前端路由:

  • frontend/src/routes/oauth/oidc/idp-callback.tsx

这个页面会:

  1. 从 URL 读取 codeerrorerror_description
  2. 通过 hasAttemptedRef 避免 React strict mode 下重复触发。
  3. code 存在时调用 useOIDCExchange()
  4. 如果回调参数非法,则跳回 /sign-in

会话持久化

useOIDCExchange() 在成功后会:

  1. 把 JWT 写入 localStorage,键名为 axonhub_access_token
  2. 更新 Zustand auth store。
  3. 保存返回的 user 信息。
  4. user.preferLanguage 切换前端语言。
  5. owner 用户跳到 /,非 owner 用户跳到 /project/playground

对应的 auth store 定义在 frontend/src/stores/authStore.ts 中,token 和 user 都会持久化到 localStorage。

身份绑定与解绑

这个 PR 还支持已登录用户手动绑定额外的 OIDC 身份。

绑定流程

前端设置页 frontend/src/features/settings/security/oidc-management.tsx 会调用:

  • GET /admin/oidc/link/:provider

后端复用普通授权流程,但会额外把当前用户 ID 缓存在 oidc_link_state:<state> 下。当 IdP 回调回来时,OIDCService.Callback(...) 会识别这是一个 link 操作,于是只创建 OIDCIdentity 记录,而不是发起新的登录会话。

绑定成功后,后端会重定向到:

/settings/profile?oidc_link=success

解绑流程

同一个设置页还支持通过 GraphQL 解绑 identity。它不属于 OIDC 登录协议本身,但会影响 GET /oauth/oidc/providers 中返回的 is_linkedlinked_identity_idlinked_email 字段。

配置说明

OIDC 配置位于 oidc.providers

示例:

oidc:
  providers:
    - name: google
      display_name: Google
      issuer_url: https://accounts.google.com
      client_id: your-client-id
      client_secret: your-client-secret
      extra_scopes: [openid, profile, email]
      jit_enabled: true
      auto_link_by_email: true
      require_email_verified: true
      enable_pkce: true
      sync_user_info: true
      icon_url: https://example.com/google.svg
      button_color: "#ffffff"

当前支持的字段

internal/server/biz/oidc.go 中当前 provider 结构支持以下字段:

字段 作用
name provider 标识,用于 /oauth/oidc/authorize/:provider 之类的路由参数。
display_name 前端展示名称,可选。
issuer_url OIDC issuer discovery 地址,go-oidc 会基于它做发现。
client_id OIDC client ID。
client_secret OIDC client secret。
extra_scopes 覆盖默认 scopes;如果为空,AxonHub 默认使用 openid profile email
jit_enabled 当无法解析到现有账号时,是否允许自动建号。
auto_link_by_email 配置中已声明,但当前实现实际上会直接按“已验证邮箱”自动绑定,并未真正读取这个开关。
require_email_verified 新用户 JIT 创建时是否强制要求邮箱已验证。
role_mappings 配置中已声明,但当前登录流程还未消费。
role_mapping_precedence 配置中已声明,但当前登录流程还未消费。
enable_pkce 是否启用 PKCE challenge/verifier。
icon_url 前端展示图标;支持远程 URL、data URL,或启动时转换成 data URL 的本地文件路径。
button_color 返回给前端的按钮样式提示色。
sync_user_info 登录/绑定时是否按 OIDC claims 同步姓名与头像。

server.public_url

OIDC 回调地址还依赖服务器基础 URL。

internal/server/api/oidc.go 中的优先级是:

  1. 优先使用 server.public_url
  2. 否则使用当前请求的 scheme://host

因此,如果 AxonHub 部署在反向代理后面,或者对外暴露的是固定公网域名,建议显式设置 server.public_url,确保发给 IdP 的 redirect URI 稳定且与 IdP 客户端配置一致。

示例:

server:
  public_url: https://axonhub.example.com

这个 PR 的 review 重点

从 reviewer 角度,这个 PR 最关键的设计点是把:

  • OIDC 协议处理放在后端
  • AxonHub 会话建立放在前端

这两件事通过一个短时有效、一次性的 exchange code 串起来。这样既避免把原始 IdP token 暴露给浏览器,又能保持现有前端基于 AxonHub JWT 的登录/会话模型不变。


OIDC Authentication Flow

This document explains how the current OIDC implementation in this PR works across the backend and frontend. It is intended as a reviewer-oriented companion to the code in internal/server/biz/oidc.go, internal/server/api/oidc.go, and the frontend OIDC login flow under frontend/src/features/auth.

Overview

AxonHub uses a backend-driven OIDC login flow:

  1. The frontend asks the backend for the authorization URL.
  2. The user is redirected to the identity provider (IdP).
  3. The IdP calls back to the AxonHub backend.
  4. The backend verifies the OIDC response and resolves the AxonHub user.
  5. The backend generates a short-lived, single-use exchange code.
  6. The backend redirects the browser to the frontend callback page.
  7. The frontend exchanges that code for an AxonHub JWT.

This means the frontend never receives the raw IdP id_token, and it does not perform OIDC token verification itself.

End-to-end flow

flowchart TD
    U[User clicks an OIDC provider<br/>on /sign-in] --> F1[Frontend calls<br/>GET /oauth/oidc/authorize/:provider]
    F1 --> B1[Backend builds authorize URL<br/>and caches state/PKCE verifier]
    B1 --> IDP[Browser redirects to IdP]
    IDP --> B2[IdP redirects to callback endpoint<br/>GET /oauth/oidc/callback or /oauth/oidc/callback/:provider]
    B2 --> B3[Backend validates state / PKCE<br/>and exchanges auth code]
    B3 --> B4[Backend verifies id_token<br/>and reads claims]
    B4 --> B5{Resolve AxonHub user}
    B5 -->|Existing issuer+subject| B6[Reuse linked user]
    B5 -->|Verified email match| B7[Link identity to existing user]
    B5 -->|No user + JIT enabled| B8[Create AxonHub user<br/>and OIDC identity]
    B6 --> B9[Generate 5-minute single-use<br/>exchange code]
    B7 --> B9
    B8 --> B9
    B9 --> F2[Backend redirects to<br/>/oauth/oidc/idp-callback?code=...]
    F2 --> F3[Frontend callback route<br/>POSTs /oauth/oidc/exchange]
    F3 --> B10[Backend validates exchange code<br/>and issues AxonHub JWT]
    B10 --> F4[Frontend stores JWT in Zustand + localStorage<br/>and redirects into the app]
Loading

Backend flow

Public routes

The OIDC HTTP endpoints are registered under /oauth/oidc in internal/server/routes.go and internal/server/api/oidc.go:

  • GET /oauth/oidc/providers
  • GET /oauth/oidc/authorize/:provider
  • GET /oauth/oidc/callback
  • GET /oauth/oidc/callback/:provider
  • POST /oauth/oidc/exchange

There is also a protected linking endpoint for already signed-in users:

  • GET /admin/oidc/link/:provider

Provider initialization

Provider configuration is defined in conf/conf.go under oidc.providers, and the runtime implementation lives in internal/server/biz/oidc.go.

At startup, NewOIDCService tries to initialize each provider with oidc.NewProvider(...) using the configured issuer_url. For each provider, it builds an OAuth2 client configuration with a backend callback path:

  • /oauth/oidc/callback when only one provider is configured
  • /oauth/oidc/callback/:provider when multiple providers are configured

The redirect URI sent to the IdP becomes absolute later by prepending the current request origin or server.public_url.

Step 1: frontend asks for the authorization URL

The frontend starts login by calling GET /oauth/oidc/authorize/:provider.

OIDCService.GetAuthorizeURL(...) then:

  1. Resolves the provider by name.
  2. Re-initializes the provider lazily if startup initialization failed.
  3. Generates a random state value.
  4. Optionally generates and caches a PKCE verifier when enable_pkce is enabled.
  5. Returns the final IdP authorization URL.

If a provider failed recently, the service applies a 60-second retry backoff before re-initializing it again.

Step 2: IdP callback reaches the backend

After the user finishes authentication at the IdP, the provider redirects back to:

  • GET /oauth/oidc/callback
  • or GET /oauth/oidc/callback/:provider

OIDCHandlers.Callback(...) extracts the incoming code and state, then calls OIDCService.Callback(...).

Inside OIDCService.Callback(...), the backend:

  1. Restores the PKCE verifier from cache if PKCE is enabled.
  2. Exchanges the authorization code with the IdP using oauth2.Config.Exchange(...).
  3. Extracts the raw id_token from the OAuth2 token response.
  4. Verifies the id_token using go-oidc.
  5. Reads claims such as email, email_verified, name, given_name, family_name, and picture.

Step 3: resolve or create the AxonHub user

The core user resolution logic is in OIDCService.resolveUser(...).

The current implementation follows this order:

  1. Existing OIDC identity: find a record by (issuer, subject) and reuse that user.
  2. Verified email match: if no identity exists but the IdP returns a verified email, look up an existing AxonHub user by email and link the OIDC identity to that user.
  3. JIT provisioning: if no user exists and jit_enabled is true, create a new AxonHub user and then create the OIDC identity record.

Additional behavior:

  • If sync_user_info is enabled, the backend updates the AxonHub user's profile fields from OIDC claims on login/link.
  • If require_email_verified is enabled, new-user provisioning requires email_verified=true.
  • Newly provisioned OIDC-only users receive the special password placeholder !OIDC_SSO_ONLY!, which blocks password login later in biz/auth.go.
  • If the resolved user is deactivated, login is rejected.

Step 4: generate a short-lived exchange code

After the backend has resolved the AxonHub user, it does not issue the final dashboard JWT directly from the IdP callback.

Instead, OIDCService.Callback(...):

  1. Generates a random exchange code.
  2. Stores userID in cache under oidc_exchange:<code>.
  3. Sets the cache entry to expire after 5 minutes.

The callback handler then redirects the browser to the frontend route:

/oauth/oidc/idp-callback?code=<exchange_code>

This extra hop keeps the OIDC protocol details on the backend while still letting the frontend finish its own session setup in a controlled way.

Step 5: frontend exchanges the code for an AxonHub JWT

The frontend callback page calls POST /oauth/oidc/exchange with the exchange code.

OIDCService.ExchangeCode(...) then:

  1. Loads the cached user ID.
  2. Rejects expired or missing exchange codes.
  3. Deletes the exchange code immediately so it can only be used once.
  4. Loads the user.

OIDCHandlers.Exchange(...) then calls AuthService.GenerateJWTToken(...), which returns an AxonHub JWT signed with the system secret key:

  • algorithm: HS256
  • expiration: 7 days
  • claims: user_id, exp

The response payload is:

{
  "data": {
    "token": "<axonhub-jwt>",
    "user": { "...": "..." }
  }
}

Frontend flow

Sign-in page

The frontend login flow is implemented in:

  • frontend/src/features/auth/data/auth.ts
  • frontend/src/routes/oauth/oidc/idp-callback.tsx
  • frontend/src/lib/api-client.ts
  • frontend/src/stores/authStore.ts

The OIDC login button flow is:

  1. useOIDCProviders() loads available providers from GET /oauth/oidc/providers.
  2. useOIDCAuthorize() calls GET /oauth/oidc/authorize/:provider.
  3. On success, the browser navigates to the returned IdP authorization URL.

Frontend callback route

After the backend callback completes, the browser lands on the public frontend route:

  • frontend/src/routes/oauth/oidc/idp-callback.tsx

That route:

  1. Reads code, error, and error_description from the URL.
  2. Prevents React strict-mode double execution via hasAttemptedRef.
  3. Calls useOIDCExchange() when code is present.
  4. Redirects back to /sign-in if the callback is invalid.

Session persistence

useOIDCExchange() handles the successful exchange response by:

  1. Storing the returned JWT in local storage under axonhub_access_token.
  2. Updating the Zustand auth store.
  3. Storing the returned user payload.
  4. Switching the UI language to user.preferLanguage.
  5. Redirecting owners to / and non-owners to /project/playground.

The auth store is defined in frontend/src/stores/authStore.ts and persists both the token and the user object in local storage.

Linking and unlinking identities

The current PR also supports manual identity linking for existing signed-in users.

Link flow

The frontend settings UI in frontend/src/features/settings/security/oidc-management.tsx calls:

  • GET /admin/oidc/link/:provider

The backend reuses the normal authorization flow, but also caches the current user ID under oidc_link_state:<state>. When the IdP callback returns, OIDCService.Callback(...) detects that cached link state and creates an OIDCIdentity record instead of logging in a new session.

When linking succeeds, the backend redirects to:

/settings/profile?oidc_link=success

Unlink flow

The same settings UI can unlink an identity through GraphQL. That behavior is separate from the OIDC login protocol itself, but it affects what GET /oauth/oidc/providers returns in the is_linked, linked_identity_id, and linked_email fields.

Configuration reference

OIDC configuration lives under oidc.providers.

Example:

oidc:
  providers:
    - name: google
      display_name: Google
      issuer_url: https://accounts.google.com
      client_id: your-client-id
      client_secret: your-client-secret
      extra_scopes: [openid, profile, email]
      jit_enabled: true
      auto_link_by_email: true
      require_email_verified: true
      enable_pkce: true
      sync_user_info: true
      icon_url: https://example.com/google.svg
      button_color: "#ffffff"

Supported fields

The provider struct currently supports these fields in internal/server/biz/oidc.go:

Field Purpose
name Provider identifier used in routes such as /oauth/oidc/authorize/:provider.
display_name Optional UI label shown on the frontend.
issuer_url OIDC issuer discovery URL used by go-oidc.
client_id OIDC client ID.
client_secret OIDC client secret.
extra_scopes Overrides the default scopes. If empty, AxonHub uses openid profile email.
jit_enabled Allows AxonHub to provision a user when no existing account can be resolved.
auto_link_by_email Declared in config, but the current implementation links by verified email regardless of this flag.
require_email_verified Requires a verified email for JIT user creation.
role_mappings Declared in config, but not currently consumed by the login flow.
role_mapping_precedence Declared in config, but not currently consumed by the login flow.
enable_pkce Enables PKCE challenge/verifier handling.
icon_url Provider icon for the frontend. Supports remote URLs, data URLs, or local file paths converted to data URLs at startup.
button_color Button styling hint returned to the frontend provider list.
sync_user_info Updates name/avatar fields from OIDC claims during login/link.

server.public_url

OIDC callback URL generation also depends on the server base URL.

internal/server/api/oidc.go uses this priority:

  1. server.public_url if configured
  2. Otherwise the incoming request origin (scheme://host)

When AxonHub is behind a reverse proxy or deployed on a public domain, server.public_url should be set so the generated redirect URI is stable and matches the IdP client configuration.

Example:

server:
  public_url: https://axonhub.example.com

What reviewers should pay attention to in this PR

From a review perspective, the most important design choice is the split between:

  • OIDC protocol handling on the backend
  • AxonHub session establishment on the frontend

That split is implemented through the short-lived exchange code. It avoids exposing raw IdP tokens to the browser, while still preserving the existing frontend login/session model based on AxonHub-issued JWTs.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request lays the groundwork for integrating OpenID Connect (OIDC) into the application. It provides the necessary infrastructure for users to authenticate using external identity providers, enhancing the system's flexibility and security by supporting modern authentication standards. The changes span across database schema, backend business logic, API endpoints, and frontend user interface components to deliver a seamless OIDC login experience.

Highlights

  • OpenID Connect (OIDC) Integration: Introduced initial support for OpenID Connect (OIDC) authentication, enabling users to sign in via external identity providers.
  • Database Schema Updates: Added a new OIDCIdentity entity to the database schema, along with its corresponding fields, edges, and indexes, to store OIDC provider information and user linkages.
  • Frontend OIDC Flow: Implemented frontend components and hooks to list available OIDC providers, initiate the authorization flow, and handle the callback and token exchange process.
  • Backend OIDC Services and API: Developed new backend services and API endpoints to manage OIDC providers, generate authorization URLs, handle callbacks, and exchange authorization codes for user sessions.
  • Configuration Options: Added configuration options for OIDC providers, including issuer URL, client ID/secret, scopes, Just-In-Time (JIT) provisioning, and PKCE support.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces initial support for OpenID Connect (OIDC), which is a significant feature for authentication. The changes are comprehensive, touching both the Go backend and the TypeScript/React frontend. This includes new API endpoints, business logic for the OIDC flow, database schema changes for OIDC identities, and UI components for the sign-in page. The overall implementation is solid, but I have identified a few areas for improvement related to frontend type safety, a potential bug in the backend redirect logic, and a minor security concern in the user provisioning process. My review comments provide specific details and suggestions for these points.

Comment thread internal/server/api/oidc.go Outdated
Comment thread frontend/src/features/auth/data/auth.ts Outdated
Comment thread frontend/src/features/auth/data/auth.ts Outdated
Comment thread frontend/src/routes/oauth/oidc/callback.tsx Outdated
Comment thread internal/server/biz/oidc.go Outdated
@looplj

looplj commented Mar 18, 2026

Copy link
Copy Markdown
Owner

感谢帮忙支持 SSO 功能。
请问下,这个测试的 provider 的 icon 和 样式,是怎么设置的,是否可以在添加 provider 的时候,可以设置对应的 icon 和颜色,符合 provider 对应品牌色,这样就可以完全不用内置知名 provider 了,全都可以自定义。

@LazuliKao

Copy link
Copy Markdown
Contributor Author

感谢帮忙支持 SSO 功能。 请问下,这个测试的 provider 的 icon 和 样式,是怎么设置的,是否可以在添加 provider 的时候,可以设置对应的 icon 和颜色,符合 provider 对应品牌色,这样就可以完全不用内置知名 provider 了,全都可以自定义。

provider 的 icon 和 样式稍后我这里添加下配置,打算支持文件路径网址base64填yaml里,icon 和颜色同

目前先测通标准OIDC,各种知名provider其实就是把认证的端点提前写好,如果之后有需要也可以预置

…default

- Add per-code mutex via sync.Map to serialize ExchangeCode redemption,
  preventing concurrent requests from both passing Get before Delete
- Delete exchange code immediately after Get (before user lookup)
- Normalize SyncRoleStrategy to 'always' in the no-DB-roles branch,
  fixing stale roles not being cleared when users are removed from
  all IdP groups
Comment thread internal/server/biz/oidc.go
Ensure that when CaseSensitive is false, the match pattern is also lowercased
before comparison, matching the behavior of group values in extractGroups.
Comment thread internal/server/biz/oidc.go
Comment on lines +148 to +151
if errorDesc != "" {
c.JSON(http.StatusBadRequest, gin.H{"error": c.Query("error_description")})
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 IdP error returns raw JSON instead of redirecting to the frontend

When the identity provider rejects the auth request (e.g., the user clicks "Cancel" on the consent screen), the IdP redirects back to this callback URL with ?error=access_denied&error_description=.... The current code responds with a raw application/json 400 response directly to the browser, so the user sees a JSON blob instead of being sent back to /sign-in. The browser never reaches the frontend idp-callback route that parses error and error_description URL params.

All other failure paths already redirect to the frontend (lines 160–163), so this case should follow the same pattern:

if errorDesc != "" {
    baseURL := h.getBaseURL(c)
    c.Redirect(http.StatusFound, fmt.Sprintf(
        "%s/oauth/oidc/idp-callback?error=%s&error_description=%s",
        baseURL,
        url.QueryEscape(errorDesc),
        url.QueryEscape(c.Query("error_description")),
    ))
    return
}

@LazuliKao

Copy link
Copy Markdown
Contributor Author

基本可以合并了,大部分bug都修好了,这个 greptile-apps 有点意思,能发现这么多bug

@looplj

looplj commented May 2, 2026

Copy link
Copy Markdown
Owner

@LazuliKao
有时间帮忙写一个 Guideline 文档,怎么配置和接入 SSO,🙏

@LazuliKao

LazuliKao commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

@LazuliKao 有时间帮忙写一个 Guideline 文档,怎么配置和接入 SSO,🙏

这个PR里面其实有文档,当时合并进去了
https://github.com/looplj/axonhub/blob/unstable/docs/zh/guides/oidc.md

不过是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.

4 participants