Skip to content

Authentication

Add user login, signup, and protected routes to your Jac application.

Prerequisites


Jac provides built-in authentication with these runtime functions:

FunctionDescription
jacLogin(username, password)Login user, returns bool
jacSignup(username, password)Register user, returns dict with success key
jacLogout()Clear auth token
jacIsLoggedIn()Check if user is authenticated
cl import from "@jac/runtime" { jacLogin, jacSignup, jacLogout, jacIsLoggedIn }

cl import from "@jac/runtime" { jacLogin, jacSignup, jacLogout, jacIsLoggedIn, useNavigate }
cl {
def:pub LoginPage() -> any {
has username: str = "";
has password: str = "";
has error: str = "";
has loading: bool = False;
navigate = useNavigate();
async def handleLogin(e: any) -> None {
e.preventDefault();
error = "";
if not username or not password {
error = "Please fill in all fields";
return;
}
loading = True;
success = await jacLogin(username, password);
loading = False;
if success {
navigate("/");
} else {
error = "Invalid credentials";
}
}
return <div style={{"maxWidth": "400px", "margin": "0 auto", "padding": "2rem"}}>
<h2>Login</h2>
<form onSubmit={handleLogin}>
{error and <div style={{"color": "red", "marginBottom": "1rem"}}>{error}</div>}
<input
type="text"
value={username}
onChange={lambda e: any -> None { username = e.target.value; }}
placeholder="Username"
style={{"width": "100%", "padding": "0.5rem", "marginBottom": "1rem"}}
/>
<input
type="password"
value={password}
onChange={lambda e: any -> None { password = e.target.value; }}
placeholder="Password"
style={{"width": "100%", "padding": "0.5rem", "marginBottom": "1rem"}}
/>
<button
type="submit"
disabled={loading}
style={{"width": "100%", "padding": "0.5rem"}}
>
{loading and "Logging in..." or "Login"}
</button>
</form>
</div>;
}
}

Authenticates a user and stores the JWT token.

cl import from "@jac/runtime" { jacLogin }
cl {
async def handleLogin() -> None {
# jacLogin returns bool (True = success, False = failure)
success = await jacLogin(username, password);
if success {
# User is now logged in, token stored automatically
navigate("/dashboard");
} else {
error = "Invalid credentials";
}
}
}

Registers a new user account.

cl import from "@jac/runtime" { jacSignup }
cl {
async def handleSignup() -> None {
# jacSignup returns dict with success key
result = await jacSignup(username, password);
if result["success"] {
# User registered and logged in
navigate("/dashboard");
} else {
error = result["error"] or "Signup failed";
}
}
}

Clears the authentication token.

cl import from "@jac/runtime" { jacLogout }
cl {
def handleLogout() -> None {
jacLogout();
# User is now logged out
navigate("/login");
}
}

Checks if user is currently authenticated.

cl import from "@jac/runtime" { jacIsLoggedIn }
cl {
def:pub NavBar() -> any {
isLoggedIn = jacIsLoggedIn();
return <nav>
{isLoggedIn and (
<button onClick={lambda -> None { handleLogout(); }}>Logout</button>
) or (
<a href="/login">Login</a>
)}
</nav>;
}
}

cl import from "@jac/runtime" {
jacLogin,
jacSignup,
jacLogout,
jacIsLoggedIn,
Router,
Routes,
Route,
Link,
useNavigate
}
cl {
# === Login Page ===
def:pub LoginPage() -> any {
has username: str = "";
has password: str = "";
has error: str = "";
has loading: bool = False;
navigate = useNavigate();
# Check if already logged in
can with entry {
if jacIsLoggedIn() {
navigate("/");
}
}
async def handleLogin(e: any) -> None {
e.preventDefault();
error = "";
if not username.trim() or not password {
error = "Please fill in all fields";
return;
}
loading = True;
success = await jacLogin(username, password);
loading = False;
if success {
navigate("/");
} else {
error = "Invalid username or password";
}
}
return <div style={{"maxWidth": "400px", "margin": "2rem auto", "padding": "2rem"}}>
<h2>Login</h2>
<form onSubmit={handleLogin}>
{error and <div style={{"color": "#dc2626", "marginBottom": "1rem"}}>{error}</div>}
<div style={{"marginBottom": "1rem"}}>
<input
type="text"
value={username}
onChange={lambda e: any -> None { username = e.target.value; }}
placeholder="Username"
style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
/>
</div>
<div style={{"marginBottom": "1rem"}}>
<input
type="password"
value={password}
onChange={lambda e: any -> None { password = e.target.value; }}
placeholder="Password"
style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
/>
</div>
<button
type="submit"
disabled={loading}
style={{
"width": "100%",
"padding": "0.75rem",
"background": "#3b82f6",
"color": "white",
"border": "none",
"borderRadius": "4px",
"cursor": "pointer"
}}
>
{loading and "Logging in..." or "Login"}
</button>
<p style={{"textAlign": "center", "marginTop": "1rem"}}>
Need an account? <Link to="/signup">Sign up</Link>
</p>
</form>
</div>;
}
# === Signup Page ===
def:pub SignupPage() -> any {
has username: str = "";
has password: str = "";
has confirmPassword: str = "";
has error: str = "";
has loading: bool = False;
navigate = useNavigate();
async def handleSignup(e: any) -> None {
e.preventDefault();
error = "";
if not username.trim() or not password {
error = "Please fill in all fields";
return;
}
if password != confirmPassword {
error = "Passwords don't match";
return;
}
if password.length < 6 {
error = "Password must be at least 6 characters";
return;
}
loading = True;
result = await jacSignup(username, password);
loading = False;
if result["success"] {
navigate("/");
} else {
error = result["error"] or "Signup failed";
}
}
return <div style={{"maxWidth": "400px", "margin": "2rem auto", "padding": "2rem"}}>
<h2>Create Account</h2>
<form onSubmit={handleSignup}>
{error and <div style={{"color": "#dc2626", "marginBottom": "1rem"}}>{error}</div>}
<div style={{"marginBottom": "1rem"}}>
<input
type="text"
value={username}
onChange={lambda e: any -> None { username = e.target.value; }}
placeholder="Username"
style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
/>
</div>
<div style={{"marginBottom": "1rem"}}>
<input
type="password"
value={password}
onChange={lambda e: any -> None { password = e.target.value; }}
placeholder="Password"
style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
/>
</div>
<div style={{"marginBottom": "1rem"}}>
<input
type="password"
value={confirmPassword}
onChange={lambda e: any -> None { confirmPassword = e.target.value; }}
placeholder="Confirm Password"
style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
/>
</div>
<button
type="submit"
disabled={loading}
style={{
"width": "100%",
"padding": "0.75rem",
"background": "#10b981",
"color": "white",
"border": "none",
"borderRadius": "4px",
"cursor": "pointer"
}}
>
{loading and "Creating account..." or "Sign Up"}
</button>
<p style={{"textAlign": "center", "marginTop": "1rem"}}>
Already have an account? <Link to="/login">Login</Link>
</p>
</form>
</div>;
}
# === Protected Dashboard ===
def:pub Dashboard() -> any {
navigate = useNavigate();
# Redirect if not logged in
can with entry {
if not jacIsLoggedIn() {
navigate("/login");
}
}
def handleLogout() -> None {
jacLogout();
navigate("/login");
}
if not jacIsLoggedIn() {
return <p>Redirecting...</p>;
}
return <div style={{"padding": "2rem"}}>
<h1>Dashboard</h1>
<p>Welcome! You are logged in.</p>
<button
onClick={lambda -> None { handleLogout(); }}
style={{
"padding": "0.5rem 1rem",
"background": "#ef4444",
"color": "white",
"border": "none",
"borderRadius": "4px",
"cursor": "pointer"
}}
>
Logout
</button>
</div>;
}
# === Main App ===
def:pub app() -> any {
return <Router>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
</Routes>
</Router>;
}
}

For file-based routing, use the built-in AuthGuard component:

cl import from "@jac/runtime" { AuthGuard, Outlet }
# pages/(auth)/layout.jac - Protects all routes in (auth) group
cl {
def:pub layout() -> any {
return <AuthGuard redirect="/login">
<Outlet />
</AuthGuard>;
}
}

The AuthGuard component:

  • Checks if user is logged in via jacIsLoggedIn()
  • If authenticated: renders child routes via <Outlet />
  • If not authenticated: redirects to the specified path

For complex apps that need shared auth state across components:

cl {
import from react { createContext, useContext }
glob AuthContext = createContext(None);
# Auth Provider component
def:pub AuthProvider(props: dict) -> JsxElement {
has user: any = None;
has loading: bool = True;
can with entry {
# Check auth status on mount
if jacIsLoggedIn() {
# Optionally fetch user data from backend
user = {"authenticated": True};
}
loading = False;
}
async def login(username: str, password: str) -> bool {
success = await jacLogin(username, password);
if success {
user = {"authenticated": True};
}
return success;
}
def logout() -> None {
jacLogout();
user = None;
}
value = {
"user": user,
"loading": loading,
"isAuthenticated": user != None,
"login": login,
"logout": logout
};
return <AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>;
}
def useAuth() -> any {
return useContext(AuthContext);
}
}

FunctionReturnsDescription
jacLogin(user, pass)boolLogin, returns True on success
jacSignup(user, pass)dictSignup, returns {success: bool, ...}
jacLogout()voidClear auth token
jacIsLoggedIn()boolCheck auth status
AuthGuardcomponentProtect routes in file-based routing