From 59f0817938ed96baa2ba8a158e574352fc6af313 Mon Sep 17 00:00:00 2001 From: chengcheng Date: Fri, 31 Jul 2026 15:28:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(identity):=20=E6=94=B6=E6=95=9B=20AI=20Gat?= =?UTF-8?q?eway=20=E7=BB=9F=E4=B8=80=E8=AE=A4=E8=AF=81=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web 登录页合并为单一统一认证入口,未指定上下文时由认证中心在登录后决策。继续接受旧客户端显式传入 platform 或 tenant,并保持回调上下文及 tenant_hint 的绑定校验。同步更新 OpenAPI 产物。\n\n验证:Go 全量测试与 go vet 通过;Web lint、141 项 Vitest 和生产构建通过;本地 platform.admin 登录及管理工作台访问通过。 --- apps/api/docs/swagger.json | 5 +- apps/api/docs/swagger.yaml | 3 +- apps/api/internal/auth/oidc_client.go | 12 +-- apps/api/internal/auth/oidc_client_test.go | 37 +++++++++ apps/api/internal/httpapi/oidc_session.go | 20 +++-- .../api/internal/httpapi/oidc_session_test.go | 80 ++++++++++++++++++- .../internal/oidcsession/login_transaction.go | 12 +-- .../oidcsession/login_transaction_test.go | 19 ++++- apps/web/src/App.tsx | 15 +--- apps/web/src/components/AuthPanel.test.tsx | 8 +- apps/web/src/components/AuthPanel.tsx | 32 +++----- apps/web/src/lib/oidc.test.ts | 6 +- apps/web/src/lib/oidc.ts | 6 +- 13 files changed, 186 insertions(+), 69 deletions(-) diff --git a/apps/api/docs/swagger.json b/apps/api/docs/swagger.json index 6792cb6..e01a284 100644 --- a/apps/api/docs/swagger.json +++ b/apps/api/docs/swagger.json @@ -5909,10 +5909,9 @@ }, { "type": "string", - "description": "显式登录上下文:platform 或 tenant", + "description": "兼容旧入口的显式登录上下文:platform 或 tenant;省略时由认证中心选择", "name": "contextType", - "in": "query", - "required": true + "in": "query" }, { "type": "string", diff --git a/apps/api/docs/swagger.yaml b/apps/api/docs/swagger.yaml index b223e20..f549047 100644 --- a/apps/api/docs/swagger.yaml +++ b/apps/api/docs/swagger.yaml @@ -7895,10 +7895,9 @@ paths: in: query name: returnTo type: string - - description: 显式登录上下文:platform 或 tenant + - description: 兼容旧入口的显式登录上下文:platform 或 tenant;省略时由认证中心选择 in: query name: contextType - required: true type: string - description: Tenant 上下文可选的稳定 Tenant UUID in: query diff --git a/apps/api/internal/auth/oidc_client.go b/apps/api/internal/auth/oidc_client.go index b3f46b1..fbdd1e9 100644 --- a/apps/api/internal/auth/oidc_client.go +++ b/apps/api/internal/auth/oidc_client.go @@ -86,14 +86,14 @@ func (c *OIDCPublicClient) AuthorizationURL( return "", errors.New("state, nonce and PKCE verifier are required") } contextType = strings.TrimSpace(contextType) - if contextType != "platform" && contextType != "tenant" { - return "", errors.New("context type must be platform or tenant") + if contextType != "" && contextType != "platform" && contextType != "tenant" { + return "", errors.New("context type must be empty, platform or tenant") } if strings.TrimSpace(tenantHint) != "" && uuid.Validate(strings.TrimSpace(tenantHint)) != nil { return "", errors.New("tenant hint must be a UUID") } - if contextType == "platform" && strings.TrimSpace(tenantHint) != "" { - return "", errors.New("platform context cannot bind a tenant hint") + if contextType != "tenant" && strings.TrimSpace(tenantHint) != "" { + return "", errors.New("only tenant context can bind a tenant hint") } config, _, err := c.configuration(ctx) if err != nil { @@ -102,7 +102,9 @@ func (c *OIDCPublicClient) AuthorizationURL( options := []oauth2.AuthCodeOption{ oidc.Nonce(nonce), oauth2.S256ChallengeOption(pkceVerifier), - oauth2.SetAuthURLParam("context_type", contextType), + } + if contextType != "" { + options = append(options, oauth2.SetAuthURLParam("context_type", contextType)) } if strings.TrimSpace(tenantHint) != "" { options = append(options, oauth2.SetAuthURLParam("tenant_hint", strings.TrimSpace(tenantHint))) diff --git a/apps/api/internal/auth/oidc_client_test.go b/apps/api/internal/auth/oidc_client_test.go index 2853258..e06682c 100644 --- a/apps/api/internal/auth/oidc_client_test.go +++ b/apps/api/internal/auth/oidc_client_test.go @@ -92,6 +92,43 @@ func TestOIDCPublicClientUsesAuthorizationCodePKCES256WithoutSecret(t *testing.T } } +func TestOIDCPublicClientOmitsOptionalContextForUnifiedLogin(t *testing.T) { + var issuer string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/.well-known/openid-configuration" { + http.NotFound(w, r) + return + } + _ = json.NewEncoder(w).Encode(map[string]string{ + "issuer": issuer, "authorization_endpoint": issuer + "/authorize", + "token_endpoint": issuer + "/token", "jwks_uri": issuer + "/jwks", + }) + })) + defer server.Close() + issuer = server.URL + + client, err := NewOIDCPublicClient(OIDCPublicClientConfig{ + AppEnv: "test", Issuer: issuer, ClientID: "gateway-public", + RedirectURI: "https://gateway.example.com/callback", + PostLogoutRedirectURI: "https://gateway.example.com/", + HTTPClient: server.Client(), + }) + if err != nil { + t.Fatal(err) + } + authorizationURL, err := client.AuthorizationURL( + context.Background(), "state", "nonce", + "test-pkce-verifier-with-at-least-43-characters-1234", "", "", + ) + if err != nil { + t.Fatal(err) + } + parsed, _ := url.Parse(authorizationURL) + if parsed.Query().Has("context_type") || parsed.Query().Has("tenant_hint") { + t.Fatalf("unified login leaked optional context binding: %v", parsed.Query()) + } +} + func TestOIDCPublicClientRejectsOfflineAccess(t *testing.T) { _, err := NewOIDCPublicClient(OIDCPublicClientConfig{ AppEnv: "production", diff --git a/apps/api/internal/httpapi/oidc_session.go b/apps/api/internal/httpapi/oidc_session.go index 12d89b2..a6e0d46 100644 --- a/apps/api/internal/httpapi/oidc_session.go +++ b/apps/api/internal/httpapi/oidc_session.go @@ -36,7 +36,7 @@ const ( // @Description Gateway 生成 state、nonce 和 PKCE S256 参数,并跳转认证中心;浏览器不接触 Token。 // @Tags auth // @Param returnTo query string false "登录后返回的站内相对路径" -// @Param contextType query string true "显式登录上下文:platform 或 tenant" +// @Param contextType query string false "兼容旧入口的显式登录上下文:platform 或 tenant;省略时由认证中心选择" // @Param tenantHint query string false "Tenant 上下文可选的稳定 Tenant UUID" // @Success 303 // @Failure 400 {object} ErrorEnvelope @@ -54,7 +54,8 @@ func (s *Server) startOIDCLogin(w http.ResponseWriter, r *http.Request) { } contextType := strings.TrimSpace(r.URL.Query().Get("contextType")) tenantHint := strings.TrimSpace(r.URL.Query().Get("tenantHint")) - validContext := contextType == "tenant" || + validContext := contextType == "" || + contextType == "tenant" || contextType == "platform" && runtime.Revision.TenantMode == "multi_tenant" validTenantHint := tenantHint == "" || @@ -141,9 +142,7 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) { s.writeOIDCTokenFailure(w, r, "ACCESS_TOKEN_INVALID", auth.OIDCValidationCategory(err), "认证中心访问令牌校验失败") return } - if identity.ContextType != transaction.ContextType || - transaction.TenantHint != "" && - identity.TenantID != transaction.TenantHint { + if !oidcLoginContextMatches(transaction, identity) { s.writeOIDCTokenFailure( w, r, "ACCESS_TOKEN_CONTEXT_MISMATCH", "STABLE_IDENTITY_CLAIMS_INVALID", @@ -190,6 +189,17 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, oidcReturnLocation(runtime.Revision.WebBaseURL, transaction.ReturnTo), http.StatusSeeOther) } +func oidcLoginContextMatches( + transaction oidcsession.LoginTransaction, + identity *auth.User, +) bool { + return identity != nil && + (transaction.ContextType == "" || + identity.ContextType == transaction.ContextType) && + (transaction.TenantHint == "" || + identity.TenantID == transaction.TenantHint) +} + func (s *Server) replaceOIDCBrowserSession( ctx context.Context, r *http.Request, diff --git a/apps/api/internal/httpapi/oidc_session_test.go b/apps/api/internal/httpapi/oidc_session_test.go index c84a01b..2a297f4 100644 --- a/apps/api/internal/httpapi/oidc_session_test.go +++ b/apps/api/internal/httpapi/oidc_session_test.go @@ -50,6 +50,29 @@ func TestStartOIDCLoginSetsEncryptedLaxTransactionAndRedirectsWithPKCE(t *testin } } +func TestStartOIDCLoginAllowsAuthCenterToSelectContext(t *testing.T) { + cipher, _ := oidcsession.NewCipher(bytes.Repeat([]byte{3}, 32)) + client := &fakeOIDCClient{authorizationURL: "https://auth.example.com/authorize"} + server := &Server{ + auth: &auth.Authenticator{OIDCVerifier: &auth.OIDCVerifier{}}, oidcClient: client, + oidcSessions: &fakeOIDCSessions{}, oidcSessionCipher: cipher, + identityTestRevision: identity.Revision{TenantMode: "multi_tenant"}, + logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + } + recorder := httptest.NewRecorder() + server.startOIDCLogin(recorder, httptest.NewRequest( + http.MethodGet, "/api/v1/auth/oidc/login?returnTo=%2Fworkspace", nil, + )) + if recorder.Code != http.StatusSeeOther || client.contextType != "" || client.tenantHint != "" { + t.Fatalf("status=%d contextType=%q tenantHint=%q", recorder.Code, client.contextType, client.tenantHint) + } + cookies := recorder.Result().Cookies() + transaction, err := cipher.DecodeLoginTransaction(cookies[0].Value, time.Now()) + if err != nil || transaction.ContextType != "" || transaction.ReturnTo != "/workspace" { + t.Fatalf("transaction=%+v err=%v", transaction, err) + } +} + func TestStartOIDCLoginRejectsOpenRedirect(t *testing.T) { cipher, _ := oidcsession.NewCipher(bytes.Repeat([]byte{3}, 32)) server := &Server{ @@ -114,10 +137,10 @@ func TestStartOIDCLoginRejectsInvalidOrSingleTenantHint(t *testing.T) { } } -func TestStartOIDCLoginRejectsMissingOrIncompatibleContext(t *testing.T) { +func TestStartOIDCLoginRejectsInvalidOrIncompatibleContext(t *testing.T) { for _, path := range []string{ - "/api/v1/auth/oidc/login", "/api/v1/auth/oidc/login?contextType=account", + "/api/v1/auth/oidc/login?tenantHint=aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "/api/v1/auth/oidc/login?contextType=platform&tenantHint=aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", } { cipher, _ := oidcsession.NewCipher(bytes.Repeat([]byte{3}, 32)) @@ -140,6 +163,59 @@ func TestStartOIDCLoginRejectsMissingOrIncompatibleContext(t *testing.T) { } } +func TestOIDCLoginContextBindingSupportsUnifiedAndExplicitEntries(t *testing.T) { + tenantID := "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + for _, test := range []struct { + name string + transaction oidcsession.LoginTransaction + identity *auth.User + want bool + }{ + { + name: "unified platform", + transaction: oidcsession.LoginTransaction{}, + identity: &auth.User{ContextType: "platform"}, + want: true, + }, + { + name: "unified tenant", + transaction: oidcsession.LoginTransaction{}, + identity: &auth.User{ContextType: "tenant", TenantID: tenantID}, + want: true, + }, + { + name: "legacy explicit platform matches", + transaction: oidcsession.LoginTransaction{ContextType: "platform"}, + identity: &auth.User{ContextType: "platform"}, + want: true, + }, + { + name: "legacy explicit platform rejects tenant", + transaction: oidcsession.LoginTransaction{ContextType: "platform"}, + identity: &auth.User{ContextType: "tenant", TenantID: tenantID}, + want: false, + }, + { + name: "tenant hint remains bound", + transaction: oidcsession.LoginTransaction{ContextType: "tenant", TenantHint: tenantID}, + identity: &auth.User{ContextType: "tenant", TenantID: tenantID}, + want: true, + }, + { + name: "tenant hint mismatch", + transaction: oidcsession.LoginTransaction{ContextType: "tenant", TenantHint: tenantID}, + identity: &auth.User{ContextType: "tenant", TenantID: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}, + want: false, + }, + } { + t.Run(test.name, func(t *testing.T) { + if got := oidcLoginContextMatches(test.transaction, test.identity); got != test.want { + t.Fatalf("oidcLoginContextMatches() = %v, want %v", got, test.want) + } + }) + } +} + func TestCompleteOIDCLoginReportsSafeTransactionFailureReason(t *testing.T) { cipher, err := oidcsession.NewCipher(bytes.Repeat([]byte{3}, 32)) if err != nil { diff --git a/apps/api/internal/oidcsession/login_transaction.go b/apps/api/internal/oidcsession/login_transaction.go index 0e51960..d01fe3a 100644 --- a/apps/api/internal/oidcsession/login_transaction.go +++ b/apps/api/internal/oidcsession/login_transaction.go @@ -42,11 +42,11 @@ func NewLoginTransactionWithContext( } contextType = strings.TrimSpace(contextType) tenantHint = strings.TrimSpace(tenantHint) - if contextType != "platform" && contextType != "tenant" { - return LoginTransaction{}, errors.New("contextType must be platform or tenant") + if contextType != "" && contextType != "platform" && contextType != "tenant" { + return LoginTransaction{}, errors.New("contextType must be empty, platform or tenant") } - if contextType == "platform" && tenantHint != "" { - return LoginTransaction{}, errors.New("platform context cannot bind a tenant hint") + if contextType != "tenant" && tenantHint != "" { + return LoginTransaction{}, errors.New("only tenant context can bind a tenant hint") } if tenantHint != "" && uuid.Validate(tenantHint) != nil { return LoginTransaction{}, errors.New("tenantHint must be a UUID") @@ -88,8 +88,8 @@ func (c *Cipher) DecodeLoginTransaction(encoded string, now time.Time) (LoginTra return LoginTransaction{}, err } if transaction.State == "" || transaction.Nonce == "" || transaction.PKCEVerifier == "" || !ValidReturnTo(transaction.ReturnTo) || - transaction.ContextType != "platform" && transaction.ContextType != "tenant" || - transaction.ContextType == "platform" && transaction.TenantHint != "" || + transaction.ContextType != "" && transaction.ContextType != "platform" && transaction.ContextType != "tenant" || + transaction.ContextType != "tenant" && transaction.TenantHint != "" || transaction.TenantHint != "" && uuid.Validate(transaction.TenantHint) != nil || transaction.CreatedAt.IsZero() || now.Before(transaction.CreatedAt.Add(-time.Minute)) || !now.Before(transaction.CreatedAt.Add(10*time.Minute)) { return LoginTransaction{}, errors.New("OIDC login transaction has expired or is invalid") diff --git a/apps/api/internal/oidcsession/login_transaction_test.go b/apps/api/internal/oidcsession/login_transaction_test.go index 1980915..fa554ad 100644 --- a/apps/api/internal/oidcsession/login_transaction_test.go +++ b/apps/api/internal/oidcsession/login_transaction_test.go @@ -54,6 +54,23 @@ func TestLoginTransactionEncryptsTenantHint(t *testing.T) { } } +func TestLoginTransactionAllowsIssuerSelectedContextWithoutTenantHint(t *testing.T) { + now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC) + cipher, _ := NewCipher(bytes.Repeat([]byte{9}, 32)) + transaction, err := NewLoginTransactionWithContext("/", "", "", now) + if err != nil { + t.Fatal(err) + } + encoded, err := cipher.EncodeLoginTransaction(transaction) + if err != nil { + t.Fatal(err) + } + decoded, err := cipher.DecodeLoginTransaction(encoded, now) + if err != nil || decoded.ContextType != "" || decoded.TenantHint != "" { + t.Fatalf("decoded transaction=%+v err=%v", decoded, err) + } +} + func TestLoginTransactionRejectsInvalidContextBinding(t *testing.T) { now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC) tenantHint := "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" @@ -61,7 +78,7 @@ func TestLoginTransactionRejectsInvalidContextBinding(t *testing.T) { contextType string tenantHint string }{ - {contextType: ""}, + {contextType: "", tenantHint: tenantHint}, {contextType: "account"}, {contextType: "platform", tenantHint: tenantHint}, } { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 8d2201e..9c45e83 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -135,7 +135,6 @@ import { loadOIDCRuntimeConfiguration, startOIDCLogin, startOIDCLogout, - type OIDCContextType, } from './lib/oidc'; import { runTask, type RunTaskOptions } from './lib/run-task'; import { AdminPage } from './pages/AdminPage'; @@ -281,7 +280,6 @@ export function App() { const [error, setError] = useState(''); const [oidcCallbackError, setOIDCCallbackError] = useState(() => consumeOIDCCallbackError()); const [oidcEnabled, setOIDCEnabled] = useState(false); - const [oidcContextTypes, setOIDCContextTypes] = useState([]); const currentCredentialRef = useRef(token); const resetAuthenticatedSessionRef = useRef<() => void>(() => undefined); currentCredentialRef.current = token; @@ -340,7 +338,6 @@ export function App() { if (cancelled) return; const identityEnabled = configuration.enabled && configuration.oidcLogin; setOIDCEnabled(identityEnabled); - setOIDCContextTypes(configuration.contextTypes); if (force && currentCredentialRef.current === OIDC_BROWSER_SESSION_CREDENTIAL) { await reconcileOIDCBrowserSessionAfterRuntimeChange({ credential: currentCredentialRef.current, @@ -1307,14 +1304,11 @@ export function App() { navigatePath('/'); } - function loginWithOIDC( - contextType: OIDCContextType = - oidcContextTypes.includes('platform') ? 'platform' : 'tenant', - ) { + function loginWithOIDC() { setState('loading'); setError(''); setOIDCCallbackError(null); - void startOIDCLogin(contextType).catch((err) => { + void startOIDCLogin().catch((err) => { setState('error'); setError(err instanceof Error ? err.message : '统一认证登录失败'); }); @@ -1327,13 +1321,12 @@ export function App() { const configuration = await loadOIDCRuntimeConfiguration(true); const identityEnabled = configuration.enabled && configuration.oidcLogin; setOIDCEnabled(identityEnabled); - setOIDCContextTypes(configuration.contextTypes); if ( loginEntryAction(identityEnabled) === 'oidc' && configuration.contextTypes.length === 1 ) { setOIDCCallbackError(null); - await startOIDCLogin(configuration.contextTypes[0], configuration); + await startOIDCLogin(undefined, configuration); return; } setState('idle'); @@ -1492,7 +1485,6 @@ export function App() { onSubmitLogin={submitLogin} onSubmitRegister={submitRegister} oidcEnabled={oidcEnabled} - oidcContextTypes={oidcContextTypes} onOIDCLogin={loginWithOIDC} /> ) @@ -1565,7 +1557,6 @@ export function App() { onSubmitLogin={submitLogin} onSubmitRegister={submitRegister} oidcEnabled={oidcEnabled} - oidcContextTypes={oidcContextTypes} onOIDCLogin={loginWithOIDC} /> ) diff --git a/apps/web/src/components/AuthPanel.test.tsx b/apps/web/src/components/AuthPanel.test.tsx index 5135efe..0a9c092 100644 --- a/apps/web/src/components/AuthPanel.test.tsx +++ b/apps/web/src/components/AuthPanel.test.tsx @@ -15,16 +15,16 @@ const baseProps = { onSubmitExternalToken: vi.fn(), onSubmitLogin: vi.fn(), onSubmitRegister: vi.fn(), - oidcContextTypes: ['platform', 'tenant'] as const, onOIDCLogin: vi.fn(), }; describe('AuthPanel', () => { - it('shows explicit platform and tenant entries when OIDC is enabled', () => { + it('shows one unified authentication entry when OIDC is enabled', () => { const html = renderToStaticMarkup(); - expect(html).toContain('使用平台账号登录'); - expect(html).toContain('使用租户账号登录'); + expect(html).toContain('使用统一认证登录'); + expect(html).not.toContain('使用平台账号登录'); + expect(html).not.toContain('使用租户账号登录'); expect(html).not.toContain('用户名或邮箱'); expect(html).not.toContain('注册账号'); expect(html).not.toContain('外部 Token'); diff --git a/apps/web/src/components/AuthPanel.tsx b/apps/web/src/components/AuthPanel.tsx index 0f17023..b344f7c 100644 --- a/apps/web/src/components/AuthPanel.tsx +++ b/apps/web/src/components/AuthPanel.tsx @@ -2,7 +2,6 @@ import type { FormEvent } from 'react'; import { LogIn, UserPlus } from 'lucide-react'; import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, PasswordInput, Tabs } from './ui'; import type { AuthMode, LoadState, LoginForm, RegisterForm } from '../types'; -import type { OIDCContextType } from '../lib/oidc'; const tabs = [ { value: 'login', label: '账号登录' }, @@ -24,8 +23,7 @@ export function AuthPanel(props: { onSubmitLogin: (event: FormEvent) => void; onSubmitRegister: (event: FormEvent) => void; oidcEnabled: boolean; - oidcContextTypes: readonly OIDCContextType[]; - onOIDCLogin: (contextType: OIDCContextType) => void; + onOIDCLogin: () => void; }) { return (
@@ -39,26 +37,14 @@ export function AuthPanel(props: { {props.oidcEnabled ? (
- {props.oidcContextTypes.includes('platform') && ( - - )} - {props.oidcContextTypes.includes('tenant') && ( - - )} +
) : ( <> diff --git a/apps/web/src/lib/oidc.test.ts b/apps/web/src/lib/oidc.test.ts index 6eaf0c0..6fbb243 100644 --- a/apps/web/src/lib/oidc.test.ts +++ b/apps/web/src/lib/oidc.test.ts @@ -7,7 +7,7 @@ describe('OIDC BFF navigation', () => { vi.resetModules(); }); - it('binds platform context and returnTo at the Gateway login endpoint', async () => { + it('lets the authentication center select context and binds returnTo', async () => { vi.stubEnv('VITE_GATEWAY_API_BASE_URL', 'https://gateway.example.com/gateway-api'); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, @@ -18,11 +18,11 @@ describe('OIDC BFF navigation', () => { location: { pathname: '/workspace', search: '?tab=wallet', hash: '#balance', assign }, }); const { startOIDCLogin } = await import('./oidc'); - await startOIDCLogin('platform'); + await startOIDCLogin(); const target = new URL(String(assign.mock.calls[0]?.[0])); expect(target.pathname).toBe('/gateway-api/api/v1/auth/oidc/login'); expect(target.searchParams.get('returnTo')).toBe('/workspace?tab=wallet#balance'); - expect(target.searchParams.get('contextType')).toBe('platform'); + expect(target.searchParams.has('contextType')).toBe(false); expect(target.searchParams.has('client_id')).toBe(false); expect(target.searchParams.has('code_challenge')).toBe(false); }); diff --git a/apps/web/src/lib/oidc.ts b/apps/web/src/lib/oidc.ts index 244d134..4d6704f 100644 --- a/apps/web/src/lib/oidc.ts +++ b/apps/web/src/lib/oidc.ts @@ -92,7 +92,7 @@ export async function loadOIDCRuntimeConfiguration(force = false): Promise