feat(identity): 接入认证中心多租户登录
支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/oidcsession"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -49,7 +50,12 @@ func (s *Server) startOIDCLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if returnTo == "" {
|
||||
returnTo = "/"
|
||||
}
|
||||
transaction, err := oidcsession.NewLoginTransaction(returnTo, time.Now())
|
||||
tenantHint := strings.TrimSpace(r.URL.Query().Get("tenantHint"))
|
||||
if tenantHint != "" && (runtime.Revision.TenantMode != "multi_tenant" || uuid.Validate(tenantHint) != nil) {
|
||||
writeError(w, http.StatusBadRequest, "租户提示无效", errorCodeOIDCLoginInvalid)
|
||||
return
|
||||
}
|
||||
transaction, err := oidcsession.NewLoginTransactionWithTenantHint(returnTo, tenantHint, time.Now())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "登录后返回地址无效", errorCodeOIDCLoginInvalid)
|
||||
return
|
||||
@@ -60,7 +66,9 @@ func (s *Server) startOIDCLogin(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusServiceUnavailable, "登录会话初始化失败,请稍后重试", errorCodeOIDCSessionStoreUnavailable)
|
||||
return
|
||||
}
|
||||
authorizationURL, err := runtime.PublicClient.AuthorizationURL(r.Context(), transaction.State, transaction.Nonce, transaction.PKCEVerifier)
|
||||
authorizationURL, err := runtime.PublicClient.AuthorizationURL(
|
||||
r.Context(), transaction.State, transaction.Nonce, transaction.PKCEVerifier, transaction.TenantHint,
|
||||
)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(r.Context(), "load OIDC authorization endpoint failed", "error", err)
|
||||
writeError(w, http.StatusServiceUnavailable, "认证中心暂时不可用,请稍后重试", "OIDC_AUTHORIZATION_UNAVAILABLE")
|
||||
@@ -124,7 +132,7 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeOIDCTokenFailure(w, r, "ID_TOKEN_INVALID", auth.OIDCValidationCategory(err), "认证中心身份令牌校验失败")
|
||||
return
|
||||
}
|
||||
projection, err := s.resolveOIDCUserProjectionForRevision(r.Context(), r, identity, runtime.Revision)
|
||||
projection, err := s.resolveOIDCUserProjectionForRuntime(r.Context(), r, identity, runtime)
|
||||
if err != nil {
|
||||
s.writeOIDCCallbackProjectionError(w, r, err)
|
||||
return
|
||||
@@ -140,6 +148,13 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeOIDCCallbackError(w, r, http.StatusServiceUnavailable, "登录会话保存失败,请稍后重试", errorCodeOIDCSessionStoreUnavailable)
|
||||
return
|
||||
}
|
||||
if err := s.replaceOIDCBrowserSession(r.Context(), r, runtime, rawSession); err != nil {
|
||||
if _, cleanupErr := runtime.Sessions.Delete(r.Context(), rawSession); cleanupErr != nil {
|
||||
s.logger.WarnContext(r.Context(), "cleanup replacement OIDC session failed", "error", cleanupErr)
|
||||
}
|
||||
s.writeOIDCCallbackError(w, r, http.StatusServiceUnavailable, "旧登录会话清理失败,请稍后重试", errorCodeOIDCSessionStoreUnavailable)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: auth.OIDCSessionCookieName, Value: rawSession, Path: "/",
|
||||
@@ -151,6 +166,34 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, oidcReturnLocation(runtime.Revision.WebBaseURL, transaction.ReturnTo), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) replaceOIDCBrowserSession(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
runtime *identityRequestRuntime,
|
||||
newRawSession string,
|
||||
) error {
|
||||
previous, err := r.Cookie(auth.OIDCSessionCookieName)
|
||||
if errors.Is(err, http.ErrNoCookie) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if previous.Value == "" || previous.Value == newRawSession {
|
||||
return nil
|
||||
}
|
||||
bundle, err := runtime.Sessions.Delete(ctx, previous.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bundle.RefreshToken != "" {
|
||||
if err := runtime.PublicClient.RevokeRefreshToken(ctx, bundle.RefreshToken); err != nil {
|
||||
s.logger.WarnContext(ctx, "revoke replaced OIDC refresh token failed", "error", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// logoutOIDCSession godoc
|
||||
// @Summary 注销 OIDC 登录会话
|
||||
// @Description 删除 Gateway Session、撤销公共 Client Refresh Token,并跳转认证中心退出地址。
|
||||
|
||||
Reference in New Issue
Block a user