feat(studio): disable auth flow for HF Spaces deployment (#4375)

This commit is contained in:
Lee Jackson 2026-03-18 05:40:03 +00:00 committed by Roland Tannous
commit 7b73882c68
4 changed files with 85 additions and 20 deletions

View file

@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import os
import secrets
from datetime import datetime, timedelta, timezone
from typing import Optional, Tuple
@ -21,7 +22,28 @@ ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60
REFRESH_TOKEN_EXPIRE_DAYS = 7
security = HTTPBearer() # Reads Authorization: Bearer <token>
security = HTTPBearer(auto_error = False) # Reads Authorization: Bearer <token>
def is_auth_disabled() -> bool:
"""
Return True when auth should be bypassed.
For the HF Spaces branch, auth is disabled by default so users can access
Studio without login/signup. You can explicitly re-enable auth with:
UNSLOTH_STUDIO_ENABLE_AUTH=1
"""
raw_enable = os.getenv("UNSLOTH_STUDIO_ENABLE_AUTH", "").strip().lower()
if raw_enable in {"1", "true", "yes", "on"}:
return False
raw = os.getenv("UNSLOTH_STUDIO_DISABLE_AUTH", "").strip().lower()
if raw in {"1", "true", "yes", "on"}:
return True
if raw in {"0", "false", "no", "off"}:
return False
return True
def _get_secret_for_subject(subject: str) -> str:
@ -103,7 +125,7 @@ def reload_secret() -> None:
async def get_current_subject(
credentials: HTTPAuthorizationCredentials = Depends(security),
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
) -> str:
"""Validate JWT and require the password-change flow to be completed."""
return await _get_current_subject(
@ -113,7 +135,7 @@ async def get_current_subject(
async def get_current_subject_allow_password_change(
credentials: HTTPAuthorizationCredentials = Depends(security),
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
) -> str:
"""Validate JWT but allow access to the password-change endpoint."""
return await _get_current_subject(
@ -123,7 +145,7 @@ async def get_current_subject_allow_password_change(
async def _get_current_subject(
credentials: HTTPAuthorizationCredentials,
credentials: Optional[HTTPAuthorizationCredentials],
*,
allow_password_change: bool,
) -> str:
@ -136,6 +158,15 @@ async def _get_current_subject(
async def secure_endpoint(current_subject: str = Depends(get_current_subject)):
...
"""
if is_auth_disabled():
return "hf-space-user"
if credentials is None:
raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED,
detail = "Missing bearer token",
)
token = credentials.credentials
subject = _decode_subject_without_verification(token)
if subject is None:

View file

@ -34,6 +34,10 @@ class AuthStatusResponse(BaseModel):
...,
description = "True if the seeded admin must still change the default password",
)
auth_disabled: bool = Field(
default = False,
description = "True when auth is bypassed (HF Spaces mode).",
)
class ChangePasswordRequest(BaseModel):

View file

@ -20,6 +20,7 @@ from auth.authentication import (
create_refresh_token,
get_current_subject,
get_current_subject_allow_password_change,
is_auth_disabled,
refresh_access_token,
)
@ -34,6 +35,14 @@ async def auth_status() -> AuthStatusResponse:
- initialized = False -> frontend should wait for the seeded admin bootstrap.
- initialized = True -> frontend should show login or force the first password change.
"""
if is_auth_disabled():
return AuthStatusResponse(
initialized = True,
default_username = storage.DEFAULT_ADMIN_USERNAME,
requires_password_change = False,
auth_disabled = True,
)
return AuthStatusResponse(
initialized = storage.is_initialized(),
default_username = storage.DEFAULT_ADMIN_USERNAME,
@ -42,6 +51,7 @@ async def auth_status() -> AuthStatusResponse:
)
if storage.is_initialized()
else True,
auth_disabled = False,
)

View file

@ -10,6 +10,22 @@ import {
refreshSession,
} from "@/features/auth";
type AuthStatus = {
initialized: boolean;
requires_password_change: boolean;
auth_disabled?: boolean;
};
async function getAuthStatus(): Promise<AuthStatus | null> {
try {
const res = await fetch("/api/auth/status");
if (!res.ok) return null;
return (await res.json()) as AuthStatus;
} catch {
return null;
}
}
async function hasActiveSession(): Promise<boolean> {
if (hasAuthToken()) return true;
if (!hasRefreshToken()) return false;
@ -17,28 +33,22 @@ async function hasActiveSession(): Promise<boolean> {
}
async function checkAuthInitialized(): Promise<boolean> {
try {
const res = await fetch("/api/auth/status");
if (!res.ok) return true; // fallback to login on error
const data = (await res.json()) as { initialized: boolean };
return data.initialized;
} catch {
return true; // fallback to login on error
}
const status = await getAuthStatus();
if (status?.auth_disabled) return true;
return status?.initialized ?? true; // fallback to login on error
}
async function checkPasswordChangeRequired(): Promise<boolean> {
try {
const res = await fetch("/api/auth/status");
if (!res.ok) return mustChangePassword();
const data = (await res.json()) as { requires_password_change: boolean };
return data.requires_password_change || mustChangePassword();
} catch {
return mustChangePassword();
}
const status = await getAuthStatus();
if (status?.auth_disabled) return false;
if (!status) return mustChangePassword();
return status.requires_password_change || mustChangePassword();
}
export async function requireAuth(): Promise<void> {
const status = await getAuthStatus();
if (status?.auth_disabled) return;
if (await hasActiveSession()) {
if (await checkPasswordChangeRequired()) {
throw redirect({ to: "/change-password" });
@ -52,11 +62,21 @@ export async function requireAuth(): Promise<void> {
}
export async function requireGuest(): Promise<void> {
const status = await getAuthStatus();
if (status?.auth_disabled) {
throw redirect({ to: getPostAuthRoute() });
}
if (!(await hasActiveSession())) return;
throw redirect({ to: getPostAuthRoute() });
}
export async function requirePasswordChangeFlow(): Promise<void> {
const status = await getAuthStatus();
if (status?.auth_disabled) {
throw redirect({ to: getPostAuthRoute() });
}
const requiresPasswordChange = await checkPasswordChangeRequired();
if (requiresPasswordChange) return;