Adding HTTP Header Authentication

This commit is contained in:
Owen
2025-10-06 10:14:02 -07:00
parent cb7c57fd03
commit 850e9a734a
13 changed files with 516 additions and 42 deletions

View File

@@ -61,6 +61,7 @@ export enum ActionsEnum {
getUser = "getUser",
setResourcePassword = "setResourcePassword",
setResourcePincode = "setResourcePincode",
setResourceHeaderAuth = "setResourceHeaderAuth",
setResourceWhitelist = "setResourceWhitelist",
getResourceWhitelist = "getResourceWhitelist",
generateAccessToken = "generateAccessToken",
@@ -194,7 +195,6 @@ export async function checkUserActionPermission(
return roleActionPermission.length > 0;
return false;
} catch (error) {
console.error("Error checking user action permission:", error);
throw createHttpError(

View File

@@ -380,6 +380,14 @@ export const resourcePassword = pgTable("resourcePassword", {
passwordHash: varchar("passwordHash").notNull()
});
export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
headerAuthId: serial("headerAuthId").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
headerAuthHash: varchar("headerAuthHash").notNull()
});
export const resourceAccessToken = pgTable("resourceAccessToken", {
accessTokenId: varchar("accessTokenId").primaryKey(),
orgId: varchar("orgId")
@@ -689,6 +697,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;

View File

@@ -6,6 +6,8 @@ import {
ResourceRule,
resourcePassword,
resourcePincode,
resourceHeaderAuth,
ResourceHeaderAuth,
resourceRules,
resources,
roleResources,
@@ -24,6 +26,7 @@ export type ResourceWithAuth = {
resource: Resource | null;
pincode: ResourcePincode | null;
password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null;
};
export type UserSessionWithUser = {
@@ -72,6 +75,10 @@ export async function getResourceByDomain(
resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId)
)
.leftJoin(
resourceHeaderAuth,
eq(resourceHeaderAuth.resourceId, resources.resourceId)
)
.where(eq(resources.fullDomain, domain))
.limit(1);
@@ -82,7 +89,8 @@ export async function getResourceByDomain(
return {
resource: result.resources,
pincode: result.resourcePincode,
password: result.resourcePassword
password: result.resourcePassword,
headerAuth: result.resourceHeaderAuth
};
}

View File

@@ -513,6 +513,16 @@ export const resourcePassword = sqliteTable("resourcePassword", {
passwordHash: text("passwordHash").notNull()
});
export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
headerAuthId: integer("headerAuthId").primaryKey({
autoIncrement: true
}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
headerAuthHash: text("headerAuthHash").notNull()
});
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
accessTokenId: text("accessTokenId").primaryKey(),
orgId: text("orgId")
@@ -728,6 +738,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;

View File

@@ -7,22 +7,21 @@ import {
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
import {
getResourceByDomain,
getUserSessionWithUser,
getUserOrgRole,
getRoleResourceAccess,
getUserResourceAccess,
getResourceRules,
getOrgLoginPage
getRoleResourceAccess,
getUserOrgRole,
getUserResourceAccess,
getOrgLoginPage,
getUserSessionWithUser
} from "@server/db/queries/verifySessionQueries";
import {
LoginPage,
Resource,
ResourceAccessToken,
ResourceHeaderAuth,
ResourcePassword,
ResourcePincode,
ResourceRule,
sessions,
users
ResourceRule
} from "@server/db";
import config from "@server/lib/config";
import { isIpInCidr } from "@server/lib/ip";
@@ -37,6 +36,7 @@ import { fromError } from "zod-validation-error";
import { getCountryCodeForIp, remoteGetCountryCodeForIp } from "@server/lib/geoip";
import { getOrgTierData } from "@server/routers/private/billing";
import { TierId } from "@server/lib/private/billing/tiers";
import { verifyPassword } from "@server/auth/password";
// We'll see if this speeds anything up
const cache = new NodeCache({
@@ -101,25 +101,28 @@ export async function verifyResourceSession(
query
} = parsedBody.data;
// Extract HTTP Basic Auth credentials if present
const clientHeaderAuth = extractBasicAuth(headers);
const clientIp = requestIp
? (() => {
logger.debug("Request IP:", { requestIp });
if (requestIp.startsWith("[") && requestIp.includes("]")) {
// if brackets are found, extract the IPv6 address from between the brackets
const ipv6Match = requestIp.match(/\[(.*?)\]/);
if (ipv6Match) {
return ipv6Match[1];
}
}
logger.debug("Request IP:", { requestIp });
if (requestIp.startsWith("[") && requestIp.includes("]")) {
// if brackets are found, extract the IPv6 address from between the brackets
const ipv6Match = requestIp.match(/\[(.*?)\]/);
if (ipv6Match) {
return ipv6Match[1];
}
}
// ivp4
// split at last colon
const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex);
}
return requestIp;
})()
// ivp4
// split at last colon
const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex);
}
return requestIp;
})()
: undefined;
logger.debug("Client IP:", { clientIp });
@@ -134,10 +137,11 @@ export async function verifyResourceSession(
const resourceCacheKey = `resource:${cleanHost}`;
let resourceData:
| {
resource: Resource | null;
pincode: ResourcePincode | null;
password: ResourcePassword | null;
}
resource: Resource | null;
pincode: ResourcePincode | null;
password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null;
}
| undefined = cache.get(resourceCacheKey);
if (!resourceData) {
@@ -152,7 +156,7 @@ export async function verifyResourceSession(
cache.set(resourceCacheKey, resourceData);
}
const { resource, pincode, password } = resourceData;
const { resource, pincode, password, headerAuth } = resourceData;
if (!resource) {
logger.debug(`Resource not found ${cleanHost}`);
@@ -209,21 +213,21 @@ export async function verifyResourceSession(
headers &&
headers[
config.getRawConfig().server.resource_access_token_headers.id
] &&
] &&
headers[
config.getRawConfig().server.resource_access_token_headers.token
]
]
) {
const accessTokenId =
headers[
config.getRawConfig().server.resource_access_token_headers
.id
];
];
const accessToken =
headers[
config.getRawConfig().server.resource_access_token_headers
.token
];
];
const { valid, error, tokenItem } = await verifyResourceAccessToken(
{
@@ -288,6 +292,18 @@ export async function verifyResourceSession(
}
}
// check for HTTP Basic Auth header
if (headerAuth && clientHeaderAuth) {
if(cache.get(clientHeaderAuth)) {
logger.debug("Resource allowed because header auth is valid (cached)");
return allowed(res);
}else if(await verifyPassword(clientHeaderAuth, headerAuth.headerAuthHash)){
cache.set(clientHeaderAuth, clientHeaderAuth);
logger.debug("Resource allowed because header auth is valid");
return allowed(res);
}
}
if (!sessions) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
@@ -800,3 +816,25 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise<boolean> {
return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase();
}
function extractBasicAuth(headers: Record<string, string> | undefined): string | undefined {
if (!headers || (!headers.authorization && !headers.Authorization)) {
return;
}
const authHeader = headers.authorization || headers.Authorization;
// Check if it's Basic Auth
if (!authHeader.startsWith("Basic ")) {
logger.debug("Authorization header is not Basic Auth");
return;
}
try {
// Extract the base64 encoded credentials
return authHeader.slice("Basic ".length);
} catch (error) {
logger.debug("Basic Auth: Failed to decode credentials", { error: error instanceof Error ? error.message : "Unknown error" });
}
}

View File

@@ -541,6 +541,13 @@ authenticated.post(
resource.setResourcePincode
);
authenticated.post(
`/resource/:resourceId/header-auth`,
verifyResourceAccess,
verifyUserHasAction(ActionsEnum.setResourceHeaderAuth),
resource.setResourceHeaderAuth
);
authenticated.post(
`/resource/:resourceId/whitelist`,
verifyResourceAccess,

View File

@@ -24,8 +24,7 @@ import {
verifyApiKeyIsRoot,
verifyApiKeyClientAccess,
verifyClientsEnabled,
verifyApiKeySiteResourceAccess,
verifyOrgAccess
verifyApiKeySiteResourceAccess
} from "@server/middlewares";
import HttpCode from "@server/types/HttpCode";
import { Router } from "express";
@@ -401,6 +400,13 @@ authenticated.post(
resource.setResourcePincode
);
authenticated.post(
`/resource/:resourceId/header-auth`,
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
resource.setResourceHeaderAuth
);
authenticated.post(
`/resource/:resourceId/whitelist`,
verifyApiKeyResourceAccess,
@@ -660,4 +666,4 @@ authenticated.put(
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.applyBlueprint),
org.applyBlueprint
);
);

View File

@@ -1,7 +1,12 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { resourcePassword, resourcePincode, resources } from "@server/db";
import {
db,
resourceHeaderAuth,
resourcePassword,
resourcePincode,
resources
} from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -23,6 +28,7 @@ export type GetResourceAuthInfoResponse = {
niceId: string;
password: boolean;
pincode: boolean;
headerAuth: boolean;
sso: boolean;
blockAccess: boolean;
url: string;
@@ -64,6 +70,14 @@ export async function getResourceAuthInfo(
resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId)
)
.leftJoin(
resourceHeaderAuth,
eq(
resourceHeaderAuth.resourceId,
resources.resourceId
)
)
.where(eq(resources.resourceId, Number(resourceGuid)))
.limit(1)
: await db
@@ -77,12 +91,21 @@ export async function getResourceAuthInfo(
resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId)
)
.leftJoin(
resourceHeaderAuth,
eq(
resourceHeaderAuth.resourceId,
resources.resourceId
)
)
.where(eq(resources.resourceGuid, resourceGuid))
.limit(1);
const resource = result?.resources;
const pincode = result?.resourcePincode;
const password = result?.resourcePassword;
const headerAuth = result?.resourceHeaderAuth;
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
@@ -100,6 +123,7 @@ export async function getResourceAuthInfo(
resourceName: resource.name,
password: password !== null,
pincode: pincode !== null,
headerAuth: headerAuth !== null,
sso: resource.sso,
blockAccess: resource.blockAccess,
url,

View File

@@ -22,3 +22,4 @@ export * from "./deleteResourceRule";
export * from "./listResourceRules";
export * from "./updateResourceRule";
export * from "./getUserResources";
export * from "./setResourceHeaderAuth";

View File

@@ -0,0 +1,101 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, resourceHeaderAuth } from "@server/db";
import { eq } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { fromError } from "zod-validation-error";
import { response } from "@server/lib";
import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
import { OpenAPITags, registry } from "@server/openApi";
const setResourceAuthMethodsParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
});
const setResourceAuthMethodsBodySchema = z
.object({
user: z.string().min(4).max(100).nullable(),
password: z.string().min(4).max(100).nullable()
})
.strict();
registry.registerPath({
method: "post",
path: "/resource/{resourceId}/header-auth",
description:
"Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.",
tags: [OpenAPITags.Resource],
request: {
params: setResourceAuthMethodsParamsSchema,
body: {
content: {
"application/json": {
schema: setResourceAuthMethodsBodySchema
}
}
}
},
responses: {}
});
export async function setResourceHeaderAuth(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = setResourceAuthMethodsParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = setResourceAuthMethodsBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
const { user, password } = parsedBody.data;
await db.transaction(async (trx) => {
await trx
.delete(resourceHeaderAuth)
.where(eq(resourceHeaderAuth.resourceId, resourceId));
if (user && password) {
const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64"));
await trx
.insert(resourceHeaderAuth)
.values({ resourceId, headerAuthHash });
}
});
return response(res, {
data: {},
success: true,
error: false,
message: "Header Authentication set successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -13,7 +13,7 @@ import {
ListResourceUsersResponse
} from "@server/routers/resource";
import { Button } from "@app/components/ui/button";
import { set, z } from "zod";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
@@ -26,9 +26,10 @@ import {
FormMessage
} from "@app/components/ui/form";
import { ListUsersResponse } from "@server/routers/user";
import { Binary, Key } from "lucide-react";
import { Binary, Key, Bot } from "lucide-react";
import SetResourcePasswordForm from "../../../../../../components/SetResourcePasswordForm";
import SetResourcePincodeForm from "../../../../../../components/SetResourcePincodeForm";
import SetResourceHeaderAuthForm from "../../../../../../components/SetResourceHeaderAuthForm";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import {
@@ -57,10 +58,13 @@ import {
SelectTrigger,
SelectValue
} from "@app/components/ui/select";
<<<<<<< HEAD
import { Separator } from "@app/components/ui/separator";
import { build } from "@server/build";
import { usePrivateSubscriptionStatusContext } from "@app/hooks/privateUseSubscriptionStatusContext";
import { TierId } from "@server/lib/private/billing/tiers";
=======
>>>>>>> 6f6c351f (Adding HTTP Header Authentication)
const UsersRolesFormSchema = z.object({
roles: z.array(
@@ -140,9 +144,12 @@ export default function ResourceAuthenticationPage() {
useState(false);
const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] =
useState(false);
const [loadingRemoveResourceHeaderAuth, setLoadingRemoveResourceHeaderAuth] =
useState(false);
const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false);
const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false);
const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false);
const usersRolesForm = useForm({
resolver: zodResolver(UsersRolesFormSchema),
@@ -429,6 +436,37 @@ export default function ResourceAuthenticationPage() {
.finally(() => setLoadingRemoveResourcePincode(false));
}
function removeResourceHeaderAuth() {
setLoadingRemoveResourceHeaderAuth(true);
api.post(`/resource/${resource.resourceId}/header-auth`, {
user: null,
password: null
})
.then(() => {
toast({
title: t("resourceHeaderAuthRemove"),
description: t("resourceHeaderAuthRemoveDescription")
});
updateAuthInfo({
headerAuth: false
});
router.refresh();
})
.catch((e) => {
toast({
variant: "destructive",
title: t("resourceErrorHeaderAuthRemove"),
description: formatAxiosError(
e,
t("resourceErrorHeaderAuthRemoveDescription")
)
});
})
.finally(() => setLoadingRemoveResourceHeaderAuth(false));
}
if (pageLoading) {
return <></>;
}
@@ -463,6 +501,20 @@ export default function ResourceAuthenticationPage() {
/>
)}
{isSetHeaderAuthOpen && (
<SetResourceHeaderAuthForm
open={isSetHeaderAuthOpen}
setOpen={setIsSetHeaderAuthOpen}
resourceId={resource.resourceId}
onSetHeaderAuth={() => {
setIsSetHeaderAuthOpen(false);
updateAuthInfo({
headerAuth: true
});
}}
/>
)}
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
@@ -778,6 +830,36 @@ export default function ResourceAuthenticationPage() {
: t("pincodeAdd")}
</Button>
</div>
{/* Header Authentication Protection */}
<div className="flex items-center justify-between border rounded-md p-2">
<div
className={`flex items-center ${!authInfo.headerAuth ? "text-muted-foreground" : "text-green-500"} space-x-2 text-sm`}
>
<Bot size="14" />
<span>
{t("resourceHeaderAuthProtection", {
status: authInfo.headerAuth
? t("enabled")
: t("disabled")
})}
</span>
</div>
<Button
variant="secondary"
size="sm"
onClick={
authInfo.headerAuth
? removeResourceHeaderAuth
: () => setIsSetHeaderAuthOpen(true)
}
loading={loadingRemoveResourceHeaderAuth}
>
{authInfo.headerAuth
? t("headerAuthRemove")
: t("headerAuthAdd")}
</Button>
</div>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>

View File

@@ -57,6 +57,7 @@ function getActionsCategories(root: boolean) {
[t('actionListAllowedResourceRoles')]: "listResourceRoles",
[t('actionSetResourcePassword')]: "setResourcePassword",
[t('actionSetResourcePincode')]: "setResourcePincode",
[t('actionSetResourceHeaderAuth')]: "setResourceHeaderAuth",
[t('actionSetResourceEmailWhitelist')]: "setResourceWhitelist",
[t('actionGetResourceEmailWhitelist')]: "getResourceWhitelist",
[t('actionCreateSiteResource')]: "createSiteResource",

View File

@@ -0,0 +1,186 @@
"use client";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { toast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import { formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { Resource } from "@server/db";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
const setHeaderAuthFormSchema = z.object({
user: z.string().min(4).max(100),
password: z.string().min(4).max(100)
});
type SetHeaderAuthFormValues = z.infer<typeof setHeaderAuthFormSchema>;
const defaultValues: Partial<SetHeaderAuthFormValues> = {
user: "",
password: ""
};
type SetHeaderAuthFormProps = {
open: boolean;
setOpen: (open: boolean) => void;
resourceId: number;
onSetHeaderAuth?: () => void;
};
export default function SetResourceHeaderAuthForm({
open,
setOpen,
resourceId,
onSetHeaderAuth
}: SetHeaderAuthFormProps) {
const api = createApiClient(useEnvContext());
const t = useTranslations();
const [loading, setLoading] = useState(false);
const form = useForm<SetHeaderAuthFormValues>({
resolver: zodResolver(setHeaderAuthFormSchema),
defaultValues
});
useEffect(() => {
if (!open) {
return;
}
form.reset();
}, [open]);
async function onSubmit(data: SetHeaderAuthFormValues) {
setLoading(true);
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/header-auth`, {
user: data.user,
password: data.password
})
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorHeaderAuthSetup'),
description: formatAxiosError(
e,
t('resourceErrorHeaderAuthSetupDescription')
)
});
})
.then(() => {
toast({
title: t('resourceHeaderAuthSetup'),
description: t('resourceHeaderAuthSetupDescription')
});
if (onSetHeaderAuth) {
onSetHeaderAuth();
}
})
.finally(() => setLoading(false));
}
return (
<>
<Credenza
open={open}
onOpenChange={(val) => {
setOpen(val);
setLoading(false);
form.reset();
}}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('resourceHeaderAuthSetupTitle')}</CredenzaTitle>
<CredenzaDescription>
{t('resourceHeaderAuthSetupTitleDescription')}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="set-header-auth-form"
>
<FormField
control={form.control}
name="user"
render={({ field }) => (
<FormItem>
<FormLabel>{t('user')}</FormLabel>
<FormControl>
<Input
autoComplete="off"
type="text"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input
autoComplete="off"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
</CredenzaClose>
<Button
type="submit"
form="set-header-auth-form"
loading={loading}
disabled={loading}
>
{t('resourceHeaderAuthSubmit')}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
);
}