Integration with Scute JS Core
Scute provides a comprehensive authentication solution for web applications through the @scute/js-core
package. This guide covers implementing multiple authentication methods including passkeys (WebAuthn), magic links, OTP verification, and OAuth providers. While the examples use React, the same patterns and API calls work with any web framework including Vue, Angular, Svelte, or vanilla JavaScript.
Example Application
A complete working example is available in the Scute JS repository. This example demonstrates all the authentication flows covered in this guide, including:
- Multi-method Authentication: Passkey, magic link, OTP, and OAuth sign-in
- Complete User Interface: Forms for each authentication method with proper state management
- Session Management: User profile display with active session management
- Device Registration: WebAuthn device registration flow with skip option
- Error Handling: Comprehensive error handling with user-friendly messages
- URL Management: Proper handling of magic link callbacks and URL cleanup
The example app uses Vite + React + TypeScript and provides a practical reference implementation that you can adapt for your own web applications regardless of framework. You can clone the repository and run the example locally to see all authentication flows in action.
Prerequisites
- Node.js 16+ and npm/yarn/pnpm
- A Scute application configured in your dashboard
- Basic knowledge of JavaScript/TypeScript and your chosen web framework
Installation
Install the Scute JS Core package:
npm install @scute/js-core
For React applications, also install React dependencies:
npm install react react-dom
npm install -D @types/react @types/react-dom # For TypeScript projects
For other frameworks, install the appropriate dependencies for your chosen framework (Vue, Angular, Svelte, etc.).
Quick Start
1. Client Configuration
Create a Scute client instance to handle all authentication operations:
// lib/scute.ts
import { createClient } from "@scute/js-core";
export const scuteClient = createClient({
appId: process.env.VITE_SCUTE_APP_ID!,
baseUrl: process.env.VITE_SCUTE_BASE_URL!,
});
2. Environment Variables
Configure your environment variables (.env
):
VITE_SCUTE_APP_ID=your_app_id_here
VITE_SCUTE_BASE_URL=https://your-scute-instance.com
Note: The
VITE_
prefix is required for Vite-based projects. Adjust the prefix according to your build tool (e.g.,REACT_APP_
for Create React App).
3. Basic Authentication Flow
Implement a complete authentication system with state management. The following example uses React, but the same authentication logic applies to any web framework:
import React, { useState, useEffect } from "react";
import {
ScuteClient,
type ScuteTokenPayload,
type ScuteUserData,
getMeaningfulError,
} from "@scute/js-core";
import { scuteClient } from "./lib/scute";
function App() {
const [currentView, setCurrentView] = useState<string>("loading");
const [user, setUser] = useState<ScuteUserData | null>(null);
useEffect(() => {
checkAuthStatus();
handleMagicLinkCallback();
}, []);
const checkAuthStatus = async () => {
const { data, error } = await scuteClient.getSession();
if (error) {
console.error("Session check failed:", error);
setCurrentView("login");
return;
}
if (data?.session?.status === "authenticated") {
setUser(data.user);
setCurrentView("dashboard");
} else {
setCurrentView("login");
}
};
const handleMagicLinkCallback = () => {
const magicToken = scuteClient.getMagicLinkToken();
if (magicToken) {
setCurrentView("verifying");
verifyMagicLink(magicToken);
}
};
// Render different views based on current state
return (
<div className="app">
{currentView === "loading" && <LoadingView />}
{currentView === "login" && <LoginView onSuccess={checkAuthStatus} />}
{currentView === "dashboard" && <DashboardView user={user} />}
{currentView === "verifying" && <VerifyingView />}
</div>
);
}
Authentication Flows
Scute supports three main authentication flows, each designed for different use cases and user preferences. All flows integrate seamlessly with passkey (WebAuthn) authentication when devices are registered.
1. Magic Link Flow
The magic link flow allows users to authenticate via email without remembering passwords. When a user enters their email address, a secure magic link is sent to their inbox. Upon clicking the link, they are automatically signed in or signed up if they don't have an account.
Initial Login Attempt
The signInOrUp
method first attempts passkey authentication. If the user has a registered device, the browser will prompt them to use their passkey instead of sending a magic link. If the user does not have a registered device, the method will send a magic link to the user's email address:
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { data, error } = await scuteClient.signInOrUp(identifier);
if (error) {
console.log("signInOrUp error");
return console.log({
data,
error,
meaningfulError: error && getMeaningfulError(error),
});
}
if (!data) {
// Passkey verified successfully - user has registered device
setComponent("profile");
} else {
setComponent("magic_sent");
}
};
Sending Magic Links
You can also force sending a magic link regardless of registered devices:
const handleSendMagicLink = async () => {
await scuteClient.sendLoginMagicLink(identifier);
setComponent("magic_sent");
};
Magic Link Confirmation
After sending the magic link, show a confirmation message:
const MagicSent = ({ identifier }: { identifier: string }) => {
return (
<div className="card">
<h5>Magic Link Sent</h5>
<p>
Please check <strong>{identifier}</strong> for the magic link.
</p>
</div>
);
};
Magic Link Verification
When users click the magic link, your application automatically detects and verifies the token:
// Detect magic link token on app load
useEffect(() => {
const magicLinkToken = scuteClient.getMagicLinkToken();
if (magicLinkToken) {
setComponent("magic_verify");
setMagicLinkToken(magicLinkToken);
}
}, [scuteClient]);
// Verification component
const MagicVerify = ({
scuteClient,
setComponent,
magicLinkToken,
setTokenPayload,
}: {
scuteClient: ScuteClient;
setComponent: (component: string) => void;
magicLinkToken: string | null;
setTokenPayload: (tokenPayload: ScuteTokenPayload | null) => void;
}) => {
const url = new URL(window.location.href);
const shouldSkipDeviceRegistration = !!url.searchParams.get(SCUTE_SKIP_PARAM);
const verificationStarted = useRef(false);
useEffect(() => {
const verifyMagicLink = async () => {
if (!magicLinkToken) {
return console.log("no magic link token found");
}
const { data, error } = await scuteClient.verifyMagicLinkToken(
magicLinkToken
);
if (error) {
console.log("verifyMagicLink error");
return console.log({
data,
error,
meaningfulError: error && getMeaningfulError(error),
});
}
setTokenPayload(data.authPayload);
setComponent("register_device");
url.searchParams.delete(SCUTE_MAGIC_PARAM);
window.history.replaceState({}, "", url.toString());
};
if (!verificationStarted.current) {
verificationStarted.current = true;
verifyMagicLink();
}
}, []);
return (
<div className="card">
<h5>Verifying Magic Link...</h5>
</div>
);
};
Device Registration
After successful magic link verification, users can be prompted to register their device for future passkey authentication. Users can also be given the option to skip device registration and proceed directly to their profile. This enables passwordless login using WebAuthn for subsequent authentications:
export const RegisterDevice = ({
scuteClient,
tokenPayload,
setComponent,
}: {
scuteClient: ScuteClient;
tokenPayload: ScuteTokenPayload | null;
setComponent: (component: string) => void;
}) => {
const handleRegisterDevice = async () => {
if (!tokenPayload) {
console.error("No token payload");
return;
}
const { error: signInError } = await scuteClient.signInWithTokenPayload(
tokenPayload
);
if (signInError) {
console.log("signInWithTokenPayload error");
console.log({
signInError,
meaningfulError: getMeaningfulError(signInError),
});
return;
}
const { data, error } = await scuteClient.addDevice();
if (error) {
console.log("addDevice error");
console.log({ data, error, meaningfulError: getMeaningfulError(error) });
return;
}
setComponent("profile");
};
const handleSkipDeviceRegistration = async () => {
if (!tokenPayload) {
console.error("No token payload");
return;
}
const { error: signInError } = await scuteClient.signInWithTokenPayload(
tokenPayload
);
if (signInError) {
console.log("signInWithTokenPayload error");
console.log({
signInError,
meaningfulError: getMeaningfulError(signInError),
});
return;
}
setComponent("profile");
};
return (
<div className="card">
<h5>Register Device</h5>
<button onClick={handleRegisterDevice}>Register Device</button>
<button onClick={handleSkipDeviceRegistration}>
Skip Device Registration
</button>
</div>
);
};
2. OTP Flow
The OTP (One-Time Password) flow works with both email addresses and phone numbers. The identifier type can be configured in your Scute dashboard. After entering their identifier, users receive a verification code that must be entered to complete authentication.
OTP Verification Form
const OtpForm = ({
scuteClient,
identifier,
setComponent,
setTokenPayload,
}: {
scuteClient: ScuteClient;
identifier: string;
setComponent: (component: string) => void;
setTokenPayload: (tokenPayload: ScuteTokenPayload | null) => void;
}) => {
const [otp, setOtp] = useState("");
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { data, error } = await scuteClient.verifyOtp(otp, identifier);
if (error) {
console.log("verifyOtp error");
console.log({ data, error, meaningfulError: getMeaningfulError(error) });
return;
}
if (data) {
setTokenPayload(data.authPayload);
setComponent("register_device");
}
};
return (
<form onSubmit={handleSubmit}>
<h5>Enter OTP</h5>
<input
type="text"
placeholder="OTP"
value={otp}
onChange={(e) => setOtp(e.target.value)}
/>
<button type="submit">Verify OTP</button>
</form>
);
};
Like the magic link flow, if the user already has a registered device, the browser will prompt them to use their passkey during the initial login attempt. After successful OTP verification, users are presented with the option to register their device for future passkey authentication.
3. OAuth Flow
The OAuth flow enables users to authenticate through third-party providers like Google. This flow is unique because after successful third-party authentication, Scute redirects back to your application with both a magic link token and a skip parameter.
Initiating OAuth Authentication
const handleSignInWithGoogle = async () => {
await scuteClient.signInWithOAuthProvider("google");
};
OAuth Callback Handling
Upon successful authentication with the third-party provider, Scute redirects back to your application with:
- A
sct_magic
parameter containing the authentication token - A
sct_sk
parameter that differentiates OAuth magic links from email magic links
Your magic verification method can handle OAuth callbacks as well. It also sends a sct_sk
parameter which you can use to determine the app behaviour based on whether it is an emailed magic link or an OAuth magic link. Below example uses the sct_sk
parameter to skip the device registration step for OAuth flows.
const shouldSkipDeviceRegistration = !!url.searchParams.get(SCUTE_SKIP_PARAM);
if (!shouldSkipDeviceRegistration && data?.authPayload) {
// Prompt for device registration (rare for OAuth)
setTokenPayload(data.authPayload);
setComponent("register_device");
} else {
// Direct sign-in (typical for OAuth flows)
setComponent("profile");
}
The sct_sk
parameter is OAuth-specific.
Session Management
Handle user sessions and authentication state:
// Get current session
const getCurrentSession = async () => {
const { data, error } = await scuteClient.getSession();
if (error) {
console.error("Failed to get session:", error);
return null;
}
return data;
};
// Sign out user
const signOut = async () => {
try {
await scuteClient.signOut();
setUser(null);
setCurrentView("login");
} catch (error) {
console.error("Sign out failed:", error);
}
};
// Revoke specific session
const revokeSession = async (sessionId: string) => {
try {
await scuteClient.revokeSession(sessionId);
// Refresh user data to update sessions list
await refreshUserData();
} catch (error) {
console.error("Failed to revoke session:", error);
}
};
Error Handling
Implement comprehensive error handling for authentication flows:
import { getMeaningfulError } from "@scute/js-core";
const handleAuthError = (error: any) => {
const meaningfulError = getMeaningfulError(error);
// Log for debugging
console.error("Authentication error:", meaningfulError);
// Show user-friendly message
setErrorMessage(
meaningfulError || "Authentication failed. Please try again."
);
// Handle specific error types
if (error.code === "INVALID_CREDENTIALS") {
// Handle invalid credentials
} else if (error.code === "NETWORK_ERROR") {
// Handle network issues
}
};
TypeScript Support
Scute provides comprehensive TypeScript definitions:
import type {
ScuteClient,
ScuteSession,
ScuteUserData,
ScuteTokenPayload,
AuthResponse,
SessionResponse,
} from "@scute/js-core";
// Use types for better development experience
const handleAuthResponse = (response: AuthResponse) => {
if (response.error) {
// Handle error
return;
}
// response.data is properly typed
console.log(response.data);
};