Add a thousand sails component
Describe the bug A clear and concise description of what the bug is.
Browser and Version
- Browser [ chrome,]
- Version [v1.0.7]
To Reproduce Steps to reproduce the behavior:
Screenshots
Additional context
To resolve the AccessTokenExpiredError and successfully add the "thousand sails" component, you need to ensure that your application can handle token renewal properly. Here is how you can handle the token renewal in your application:
- Check for Token Expiry: When you receive a
403or401error, check if the token has expired. - Renew the Access Token: Attempt to renew the access token using the
renewAccessTokenfunction. - Retry the Failed Request: If the token is successfully renewed, retry the failed request with the new token.
Here is the relevant code that demonstrates how to handle the AccessTokenExpiredError:
import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { renewAccessToken } from ".";
import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
baseURL: "",
});
function ApiInterceptor() {
const setErrorData = useAlertStore((state) => state.setErrorData);
let { accessToken, login, logout, authenticationErrorCount, autoLogin } =
useContext(AuthContext);
const cookies = new Cookies();
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 403 || error.response?.status === 401) {
if (!autoLogin) {
if (error?.config?.url?.includes("github")) {
return Promise.reject(error);
}
const stillRefresh = checkErrorCount();
if (!stillRefresh) {
return Promise.reject(error);
}
const acceptedRequest = await tryToRenewAccessToken(error);
const accessToken = cookies.get("access_token_lf");
if (!accessToken && error?.config?.url?.includes("login")) {
return Promise.reject(error);
}
return acceptedRequest;
}
}
await clearBuildVerticesState(error);
return Promise.reject(error);
}
);
const isAuthorizedURL = (url) => {
const authorizedDomains = [
"https://raw.githubusercontent.com/langflow-ai/langflow_examples/main/examples",
"https://api.github.com/repos/langflow-ai/langflow_examples/contents/examples",
"https://api.github.com/repos/langflow-ai/langflow",
"auto_login",
];
const authorizedEndpoints = ["auto_login"];
try {
const parsedURL = new URL(url);
const isDomainAllowed = authorizedDomains.some(
(domain) => parsedURL.origin === new URL(domain).origin
);
const isEndpointAllowed = authorizedEndpoints.some((endpoint) =>
parsedURL.pathname.includes(endpoint)
);
return isDomainAllowed || isEndpointAllowed;
} catch (e) {
// Invalid URL
return false;
}
};
// Request interceptor to add access token to every request
const requestInterceptor = api.interceptors.request.use(
(config) => {
const accessToken = cookies.get("access_token_lf");
if (accessToken && !isAuthorizedURL(config?.url)) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
return () => {
// Clean up the interceptors when the component unmounts
api.interceptors.response.eject(interceptor);
api.interceptors.request.eject(requestInterceptor);
};
}, [accessToken, setErrorData]);
function checkErrorCount() {
authenticationErrorCount = authenticationErrorCount + 1;
if (authenticationErrorCount > 3) {
authenticationErrorCount = 0;
logout();
return false;
}
return true;
}
async function tryToRenewAccessToken(error: AxiosError) {
try {
if (window.location.pathname.includes("/login")) return;
const res = await renewAccessToken();
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token);
}
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token_lf"
)}`;
const response = await axios.request(error.config);
return response;
}
} catch (error) {
clearBuildVerticesState(error);
logout();
return Promise.reject("Authentication error");
}
}
async function clearBuildVerticesState(error) {
if (error?.response?.status === 500) {
const vertices = useFlowStore.getState().verticesBuild;
useFlowStore
.getState()
.updateBuildStatus(vertices?.verticesIds ?? [], BuildStatus.BUILT);
useFlowStore.getState().setIsBuilding(false);
}
}
return null;
}
export { ApiInterceptor, api };
Additionally, ensure that your authentication settings are correctly configured. Here is an example of the AuthSettings class from the langflow project:
class AuthSettings(BaseSettings):
# Login settings
CONFIG_DIR: str
SECRET_KEY: SecretStr = Field(
default=SecretStr(""),
description="Secret key for JWT. If not provided, a random one will be generated.",
frozen=False,
)
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_SECONDS: int = 60 * 60 # 1 hour
REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 days
# API Key to execute /process endpoint
API_KEY_ALGORITHM: str = "HS256"
API_V1_STR: str = "/api/v1"
# If AUTO_LOGIN = True
# > The application does not request login and logs in automatically as a super user.
AUTO_LOGIN: bool = True
NEW_USER_IS_ACTIVE: bool = False
SUPERUSER: str = DEFAULT_SUPERUSER
SUPERUSER_PASSWORD: str = DEFAULT_SUPERUSER_PASSWORD
REFRESH_SAME_SITE: Literal["lax", "strict", "none"] = "none"
"""The SameSite attribute of the refresh token cookie."""
REFRESH_SECURE: bool = True
"""The Secure attribute of the refresh token cookie."""
REFRESH_HTTPONLY: bool = True
"""The HttpOnly attribute of the refresh token cookie."""
ACCESS_SAME_SITE: Literal["lax", "strict", "none"] = "lax"
"""The SameSite attribute of the access token cookie."""
ACCESS_SECURE: bool = False
"""The Secure attribute of the access token cookie."""
ACCESS_HTTPONLY: bool = False
"""The HttpOnly attribute of the access token cookie."""
COOKIE_DOMAIN: str | None = None
"""The domain attribute of the cookies. If None, the domain is not set."""
pwd_context: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto")
class Config:
validate_assignment = True
extra = "ignore"
env_prefix = "LANGFLOW_"
def reset_credentials(self):
self.SUPERUSER = DEFAULT_SUPERUSER
self.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
@field_validator("SUPERUSER", "SUPERUSER_PASSWORD", mode="before")
@classmethod
def validate_superuser(cls, value, info):
if info.data.get("AUTO_LOGIN"):
if value != DEFAULT_SUPERUSER:
value = DEFAULT_SUPERUSER
logger.debug("Resetting superuser to default value")
if info.data.get("SUPERUSER_PASSWORD") != DEFAULT_SUPERUSER_PASSWORD:
info.data["SUPERUSER_PASSWORD"] = DEFAULT_SUPERUSER_PASSWORD
logger.debug("Resetting superuser password to default value")
return value
return value
@field_validator("SECRET_KEY", mode="before")
@classmethod
def get_secret_key(cls, value, info):
config_dir = info.data.get("CONFIG_DIR")
if not config_dir:
logger.debug("No CONFIG_DIR provided, not saving secret key")
return value or secrets.token_urlsafe(32)
secret_key_path = Path(config_dir) / "secret_key"
if value:
logger.debug("Secret key provided")
secret_value = value.get_secret_value() if isinstance(value, SecretStr) else value
write_secret_to_file(secret_key_path, secret_value)
else:
logger.debug("No secret key provided, generating a random one")
if secret_key_path.exists():
value = read_secret_from_file(secret_key_path)
logger.debug("Loaded secret key")
if not value:
value = secrets.token_urlsafe(32)
write_secret_to_file(secret_key_path, value)
logger.debug("Saved secret key")
else:
value = secrets.token_urlsafe(32)
write_secret_to_file(secret_key_path, value)
logger.debug("Saved secret key")
return value if isinstance(value, SecretStr) else SecretStr(value)
Additionally, here is the relevant code from the langflow project that handles login, auto-login, and token refresh:
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from fastapi.security import OAuth2PasswordRequestForm
from langflow.api.v1.schemas import Token
from langflow.services.auth.utils import (
authenticate_user,
create_refresh_token,
create_user_longterm_token,
create_user_tokens,
)
from langflow.services.deps import (
get_session,
get_settings_service,
get_variable_service,
)
from langflow.services.settings.manager import SettingsService
from langflow.services.variable.service import VariableService
from sqlmodel import Session
router = APIRouter(tags=["Login"])
@router.post("/login", response_model=Token)
async def login_to_get_access_token(
response: Response,
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_session),
settings_service=Depends(get_settings_service),
variable_service: VariableService = Depends(get_variable_service),
):
auth_settings = settings_service.auth_settings
try:
user = authenticate_user(form_data.username, form_data.password, db)
except Exception as exc:
if isinstance(exc, HTTPException):
raise exc
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(exc),
) from exc
if user:
tokens = create_user_tokens(user_id=user.id, db=db, update_last_login=True)
response.set_cookie(
"refresh_token_lf",
tokens["refresh_token"],
httponly=auth_settings.REFRESH_HTTPONLY,
samesite=auth_settings.REFRESH_SAME_SITE,
secure=auth_settings.REFRESH_SECURE,
expires=auth_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
)
response.set_cookie(
"access_token_lf",
tokens["access_token"],
httponly=auth_settings.ACCESS_HTTPONLY,
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
)
variable_service.initialize_user_variables(user.id, db)
return tokens
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
@router.get("/auto_login")
async def auto_login(
response: Response,
db: Session = Depends(get_session),
settings_service=Depends(get_settings_service),
variable_service: VariableService = Depends(get_variable_service),
):
auth_settings = settings_service.auth_settings
if settings_service.auth_settings.AUTO_LOGIN:
user_id, tokens = create_user_longterm_token(db)
response.set_cookie(
"access_token_lf",
tokens["access_token"],
httponly=auth_settings.ACCESS_HTTPONLY,
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=None, # Set to None to make it a session cookie
)
variable_service.initialize_user_variables(user_id, db)
return tokens
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"message": "Auto login is disabled. Please enable it in the settings",
"auto_login": False,
},
)
@router.post("/refresh")
async def refresh_token(
request: Request,
response: Response,
settings_service: "SettingsService" = Depends(get_settings_service),
):
auth_settings = settings_service.auth_settings
token = request.cookies.get("refresh_token_lf")
if token:
tokens = create_refresh_token(token)
response.set_cookie(
"refresh_token_lf",
tokens["refresh_token"],
httponly=auth_settings.REFRESH_HTTPONLY,
samesite=auth_settings.REFRESH_SAME_SITE,
secure=auth_settings.REFRESH_SECURE,
expires=auth_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
)
response.set_cookie(
"access_token_lf",
tokens["access_token"],
httponly=auth_settings.ACCESS_HTTPONLY,
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
)
return tokens
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
headers={"WWW-Authenticate": "Bearer"},
)
@router.post("/logout")
async def logout(response: Response):
response.delete_cookie("refresh_token_lf")
response.delete_cookie("access_token_lf")
return {"message": "Logout successful"}
This code sets up an Axios interceptor to handle 403 and 401 errors by attempting to renew the access token and retrying the failed request. Make sure to integrate this logic into your application to handle token expiration and ensure smooth operation. Additionally, ensure that your authentication settings are correctly configured to support token renewal and auto-login if necessary [1][2][3].
To continue talking to Dosu, mention @dosu.
@alex-ge123
Try it with the new version of Langflow and let me know if the problem still happens.
Thank you for your contribution! This issue will be closed. If you have any questions or encounter another problem, please open a new issue and we will be ready to assist you.