Skip to main content

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.

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");
}
};

You can also force sending a magic link regardless of registered devices:

const handleSendMagicLink = async () => {
await scuteClient.sendLoginMagicLink(identifier);
setComponent("magic_sent");
};

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>
);
};

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);
};