show sso page in browser for gateway resource

This commit is contained in:
miloschwartz
2026-08-12 11:32:22 -04:00
parent 83035753af
commit 115c3cbf07
8 changed files with 45 additions and 58 deletions
+5 -5
View File
@@ -1699,16 +1699,16 @@
"virtualApiKeysFilterUnassigned": "Unassigned",
"myVirtualApiKeysTitle": "Your API Keys",
"myVirtualApiKeysDescription": "View your identity key and any virtual API keys attributed to you in this organization",
"myVirtualApiKeysResourceTitle": "Your API Keys for This Resource",
"myVirtualApiKeysResourceDescription": "View your identity key and virtual API keys attributed to you that can access this resource",
"myVirtualApiKeysResourceTitle": "Your API Keys for {resourceName}",
"myVirtualApiKeysResourceDescription": "View your identity key and virtual API keys attributed to you that can access {resourceName}",
"myVirtualApiKeysIdentityTitle": "Identity Key",
"myVirtualApiKeysIdentityHeadline": "Your Personal API Key",
"myVirtualApiKeysIdentityDescription": "Your personal key for this organization. It is unique to your account and used to identify you when calling AI Gateway resources.",
"myVirtualApiKeysIdentityResourceHeadline": "Your Personal API Key for This Resource",
"myVirtualApiKeysIdentityResourceDescription": "Your personal key for this organization. Use it to call this AI Gateway resource.",
"myVirtualApiKeysIdentityResourceHeadline": "Your Personal API Key for {resourceName}",
"myVirtualApiKeysIdentityResourceDescription": "Your personal key for this organization. Use it to call {resourceName}.",
"myVirtualApiKeysManualTitle": "Attributed Keys",
"myVirtualApiKeysManualDescription": "Manual virtual API keys an admin associated with your account",
"myVirtualApiKeysManualResourceDescription": "Manual virtual API keys associated with your account that can access this resource",
"myVirtualApiKeysManualResourceDescription": "Manual virtual API keys associated with your account that can access {resourceName}",
"myVirtualApiKeysManualEmpty": "No attributed keys yet",
"myVirtualApiKeysKindUser": "Identity",
"myVirtualApiKeysKindManual": "Manual",
+4 -10
View File
@@ -329,16 +329,10 @@ export async function verifyResourceSession(
// Only offer a browser redirect to clients that can actually follow one and log in
// (an interactive browser). Non-browser clients (curl, scripts, bots, etc.) just get
// an unauthorized response from Badger instead of a login redirect URL.
// Inference browsers go to the dashboard keys page (not back to the inference host)
// so a valid session cannot create a redirect loop.
const redirectPath = clientIsBrowser
? mode === "inference"
? `/${resource.orgId}/resource/${encodeURIComponent(
resource.resourceGuid
)}/keys`
: `/auth/resource/${encodeURIComponent(
resource.resourceGuid
)}?redirect=${encodeURIComponent(originalRequestURL)}`
? `/auth/resource/${encodeURIComponent(
resource.resourceGuid
)}?redirect=${encodeURIComponent(originalRequestURL)}`
: undefined;
// Virtual API keys for public inference resources (provider-style auth headers).
@@ -414,7 +408,7 @@ export async function verifyResourceSession(
parsedBody.data
);
return notAllowed(res, redirectPath);
return notAllowed(res, redirectPath, resource.orgId);
}
// check for access token in headers
@@ -123,10 +123,12 @@ export async function listMyVirtualApiKeys(
}
let resourceId: number | undefined;
let resourceName: string | undefined;
if (resourceGuid) {
const [resource] = await db
.select({
resourceId: resources.resourceId
resourceId: resources.resourceId,
name: resources.name
})
.from(resources)
.where(
@@ -147,6 +149,7 @@ export async function listMyVirtualApiKeys(
}
resourceId = resource.resourceId;
resourceName = resource.name;
}
const { key: userKeyRow } = await getOrCreateUserVirtualApiKey({
@@ -203,7 +206,8 @@ export async function listMyVirtualApiKeys(
userKey: toKeyWithResources(userKeyRow, resourceIdsByKey),
manualKeys: manualRows.map((row) =>
toKeyWithResources(row, resourceIdsByKey)
)
),
...(resourceName !== undefined ? { resourceName } : {})
},
success: true,
error: false,
+1
View File
@@ -22,6 +22,7 @@ export type CreateOrEditVirtualApiKeyResponse = {
export type ListMyVirtualApiKeysResponse = {
userKey: VirtualApiKeyWithResources;
manualKeys: VirtualApiKeyWithResources[];
resourceName?: string | null;
};
export type GetMyVirtualApiKeyResponse = {
@@ -18,7 +18,7 @@ import { cache } from "react";
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations();
return {
title: t("myVirtualApiKeysResourceTitle")
title: t("myVirtualApiKeysTitle")
};
}
@@ -42,7 +42,7 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) {
if (!user) {
redirect(
`/auth/login?redirect=/${orgId}/resource/${resourceGuid}/keys`
`/auth/resource/${encodeURIComponent(resourceGuid)}?redirect=${encodeURIComponent(`/${orgId}/resource/${resourceGuid}/keys`)}`
);
}
@@ -113,11 +113,7 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) {
launcherMode
showViewAsAdmin={isAdminOrOwner}
>
<UserVirtualApiKeys
orgId={orgId}
resourceGuid={resourceGuid}
initialData={keysData}
/>
<UserVirtualApiKeys orgId={orgId} initialData={keysData} />
</Layout>
</UserProvider>
);
+9 -13
View File
@@ -163,17 +163,11 @@ export default async function ResourceAuthPage(props: {
if (user && !user.emailVerified && env.flags.emailVerificationRequired) {
redirect(
`/auth/verify-email?redirect=${encodeURIComponent(
isInference
? keysPath
: `/auth/resource/${authInfo.resourceGuid}`
`/auth/resource/${authInfo.resourceGuid}`
)}`
);
}
if (isInference && !user) {
redirect(`/auth/login?redirect=${encodeURIComponent(keysPath)}`);
}
const cookie = await authCookieHeader();
// Check org policy compliance before proceeding
@@ -192,9 +186,7 @@ export default async function ResourceAuthPage(props: {
// If user is not compliant with org policies, show policy requirements
if (orgPolicyCheck && !orgPolicyCheck.allowed && orgPolicyCheck.policies) {
const resourceAuthPageUrl = isInference
? keysPath
: `/auth/resource/${authInfo.resourceGuid}${redirectUrl !== authInfo.url ? `?redirect=${encodeURIComponent(redirectUrl)}` : ""}`;
const resourceAuthPageUrl = `/auth/resource/${authInfo.resourceGuid}${redirectUrl !== authInfo.url ? `?redirect=${encodeURIComponent(redirectUrl)}` : ""}`;
return (
<div className="w-full max-w-md">
<OrgPolicyRequired
@@ -208,10 +200,14 @@ export default async function ResourceAuthPage(props: {
// Inference resources never establish a resource session on the inference
// host. Authenticated users retrieve their virtual API key on the dashboard.
if (isInference) {
if (isInference && user) {
redirect(keysPath);
}
// After password/pincode/SSO, do not send the browser back to the
// inference host (session alone cannot pass Badger). Land on keys instead.
const postAuthRedirect = isInference ? keysPath : redirectUrl;
if (!hasAuth) {
// no authentication so always go straight to the resource
redirect(redirectUrl);
@@ -296,7 +292,7 @@ export default async function ResourceAuthPage(props: {
<AutoLoginHandler
resourceId={authInfo.resourceId}
skipToIdpId={authInfo.skipToIdpId}
redirectUrl={redirectUrl}
redirectUrl={postAuthRedirect}
orgId={build === "saas" ? authInfo.orgId : undefined}
/>
);
@@ -334,7 +330,7 @@ export default async function ResourceAuthPage(props: {
name: authInfo.resourceName,
id: authInfo.resourceId
}}
redirect={redirectUrl}
redirect={postAuthRedirect}
idps={loginIdps}
orgId={build === "saas" ? authInfo.orgId : undefined}
branding={
+11 -20
View File
@@ -4,11 +4,9 @@ import { useState } from "react";
import { useTranslations } from "next-intl";
import { AxiosResponse } from "axios";
import moment from "moment";
import { Badge } from "@app/components/ui/badge";
import { Button } from "@app/components/ui/button";
import CopyTextBox from "@app/components/CopyTextBox";
import CopyToClipboard from "@app/components/CopyToClipboard";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
SettingsContainer,
SettingsFormCell,
@@ -34,7 +32,6 @@ import {
type UserVirtualApiKeysProps = {
orgId: string;
resourceGuid?: string;
initialData: ListMyVirtualApiKeysResponse;
};
@@ -131,12 +128,12 @@ function IdentityKeyCenterpiece({
orgId,
virtualApiKeyId,
lastChars,
resourceGuid
resourceName
}: {
orgId: string;
virtualApiKeyId: string;
lastChars: string;
resourceGuid?: string;
resourceName?: string | null;
}) {
const t = useTranslations();
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
@@ -145,11 +142,11 @@ function IdentityKeyCenterpiece({
virtualApiKeyId
);
const displayValue = credential ?? preview;
const headline = resourceGuid
? t("myVirtualApiKeysIdentityResourceHeadline")
const headline = resourceName
? t("myVirtualApiKeysIdentityResourceHeadline", { resourceName })
: t("myVirtualApiKeysIdentityHeadline");
const description = resourceGuid
? t("myVirtualApiKeysIdentityResourceDescription")
const description = resourceName
? t("myVirtualApiKeysIdentityResourceDescription", { resourceName })
: t("myVirtualApiKeysIdentityDescription");
return (
@@ -220,17 +217,10 @@ function ManualKeyRow({
export default function UserVirtualApiKeys({
orgId,
resourceGuid,
initialData
}: UserVirtualApiKeysProps) {
const t = useTranslations();
const title = resourceGuid
? t("myVirtualApiKeysResourceTitle")
: t("myVirtualApiKeysTitle");
const description = resourceGuid
? t("myVirtualApiKeysResourceDescription")
: t("myVirtualApiKeysDescription");
const resourceName = initialData.resourceName;
return (
<>
@@ -239,7 +229,7 @@ export default function UserVirtualApiKeys({
orgId={orgId}
virtualApiKeyId={initialData.userKey.virtualApiKeyId}
lastChars={initialData.userKey.lastChars}
resourceGuid={resourceGuid}
resourceName={resourceName}
/>
{initialData.manualKeys.length > 0 ? (
@@ -249,9 +239,10 @@ export default function UserVirtualApiKeys({
{t("myVirtualApiKeysManualTitle")}
</SectionTitle>
<SettingsSectionDescription>
{resourceGuid
{resourceName
? t(
"myVirtualApiKeysManualResourceDescription"
"myVirtualApiKeysManualResourceDescription",
{ resourceName }
)
: t("myVirtualApiKeysManualDescription")}
</SettingsSectionDescription>
@@ -202,7 +202,12 @@ export function LauncherInferenceApiKeysSection({
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t(
"myVirtualApiKeysManualResourceDescription"
"myVirtualApiKeysManualResourceDescription",
{
resourceName:
data.resourceName ??
t("resource")
}
)}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>