Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions passwordless-auth-headless/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
133 changes: 133 additions & 0 deletions passwordless-auth-headless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Passwordless Authentication Sample App

A simple Next.js application demonstrating passwordless authentication using Scalekit with support for both verification codes and magic links.

## Features

- **Dual Authentication**: Support for OTP (One-Time Password) and Magic Links
- **Flexible Flow**: Users can choose between entering a code or clicking a magic link
- **Simple UI**: Clean, modern interface with step-by-step flow
- **Secure Sessions**: HTTP-only cookies with proper security settings
- **Error Handling**: User-friendly error messages and validation

## How it Works

1. **Email Entry**: User enters their email address
2. **Email Delivery**: A verification email is sent with code and/or magic link
3. **Authentication Options**:
- Enter the 6-digit verification code in the UI, OR
- Click the magic link directly in the email
4. **Dashboard Access**: Successfully authenticated users are redirected to a dashboard

## Authentication Types

The app supports three passwordless authentication types based on your Scalekit configuration:

- **OTP**: Only verification codes (6-digit numbers)
- **LINK**: Only magic links (clickable URLs)
- **LINK_OTP**: Both verification codes and magic links simultaneously

## Tech Stack

- **Frontend**: Next.js 14 with TypeScript and Tailwind CSS
- **Authentication**: Scalekit SDK for passwordless authentication
- **Styling**: Tailwind CSS for modern, responsive design

## Getting Started

### Prerequisites

- Node.js 18+
- Scalekit account and environment setup

### Environment Variables

Create a `.env.local` file with the following variables:

```env
# Scalekit Configuration
SCALEKIT_ENVIRONMENT_URL=your_environment_url
SCALEKIT_CLIENT_ID=your_client_id
SCALEKIT_CLIENT_SECRET=your_client_secret

# App Configuration
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

### Installation

1. Install dependencies:

```bash
npm install
# or
pnpm install
```

2. Run the development server:

```bash
npm run dev
# or
pnpm dev
```

3. Open [http://localhost:3000](http://localhost:3000) in your browser

## Project Structure

```
├── app/
│ ├── api/auth/ # Authentication API routes
│ │ ├── callback/ # OAuth callback handler
│ │ ├── initiate/ # Start authentication flow
│ │ ├── resend/ # Resend verification email
│ │ ├── verify/ # Verify OTP codes
│ │ └── verify-magic-link/ # Handle magic link verification
│ ├── dashboard/ # Protected dashboard page
│ ├── login/ # Login page
│ └── page.tsx # Home page
├── lib/
│ └── scalekit.ts # Scalekit SDK configuration
└── README.md
```

## API Endpoints

- `POST /api/auth/initiate` - Start passwordless authentication
- `POST /api/auth/verify` - Verify OTP codes
- `GET /api/auth/verify-magic-link` - Handle magic link verification
- `POST /api/auth/resend` - Resend verification email
- `GET /api/auth/callback` - Handle OAuth callback

## Scalekit Configuration

To enable different authentication types, configure your Scalekit dashboard:

1. Navigate to **Authentication > Auth Methods**
2. Locate the **Passwordless** section
3. Choose your preferred authentication type:
- **OTP**: Only verification codes
- **LINK**: Only magic links
- **LINK_OTP**: Both codes and magic links

## Customization

This is a sample application designed to be simple and educational. You can:

- Modify the UI components in the `app/` directory
- Add additional authentication flows
- Implement user management features
- Add more protected routes
- Customize error handling and validation
- Configure different passwordless types in Scalekit dashboard

## Learn More

- [Scalekit Documentation](https://docs.scalekit.com)
- [Next.js Documentation](https://nextjs.org/docs)
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)

## License

This project is open source and available under the [MIT License](LICENSE).
68 changes: 68 additions & 0 deletions passwordless-auth-headless/app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from 'next/server';
import { scalekit, scalekitConfig, handleScalekitError } from '@/lib/scalekit';

export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get('code');
const error = searchParams.get('error');
const errorDescription = searchParams.get('error_description');

// Handle OAuth errors
if (error) {
console.error('OAuth error:', error, errorDescription);
return NextResponse.redirect(
`${scalekitConfig.appUrl}/login?error=${encodeURIComponent(
errorDescription || error
)}`
);
}

// Validate authorization code
if (!code) {
return NextResponse.redirect(
`${scalekitConfig.appUrl}/login?error=No authorization code received`
);
}

// Authenticate with the authorization code
const result = await scalekit.authenticateWithCode(
code,
scalekitConfig.redirectUri
);

// Create user session data
const userSession = {
email: result.user.email,
idToken: result.idToken,
accessToken: result.accessToken,
expiresIn: result.expiresIn,
};

// Create redirect response to dashboard
const response = NextResponse.redirect(
`${scalekitConfig.appUrl}/dashboard?user=${encodeURIComponent(
JSON.stringify(userSession)
)}`
);

// Set secure session cookie
response.cookies.set('user-session', JSON.stringify(userSession), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: result.expiresIn,
});

return response;
} catch (error: any) {
// Handle authentication errors
const { error: errorMessage } = handleScalekitError(
error,
'authentication callback'
);
return NextResponse.redirect(
`${scalekitConfig.appUrl}/login?error=${encodeURIComponent(errorMessage)}`
);
}
}
45 changes: 45 additions & 0 deletions passwordless-auth-headless/app/api/auth/initiate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
import { scalekit, scalekitConfig, handleScalekitError } from '@/lib/scalekit';

export async function POST(request: NextRequest) {
try {
const { email } = await request.json();

// Validate email input
if (!email) {
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
}

// Send passwordless authentication email
const result = await scalekit.passwordless.sendPasswordlessEmail(email, {
expiresIn: 100, // Code expires in 100 seconds
magiclinkAuthUri: scalekitConfig.magiclinkAuthUri,
});

// Create response with auth request ID
const response = NextResponse.json({
success: true,
message:
'Verification email sent! You can enter the code or click the magic link.',
authReqId: result.authRequestId,
passwordlessType: result.passwordlessType,
});

// Set auth request ID in cookie for magic link verification
response.cookies.set('auth-request-id', result.authRequestId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 300, // 5 minutes (same as code expiry)
});

return response;
} catch (error: any) {
// Handle Scalekit errors gracefully
const { error: errorMessage, status } = handleScalekitError(
error,
'authentication initiation'
);
return NextResponse.json({ error: errorMessage }, { status });
}
}
31 changes: 31 additions & 0 deletions passwordless-auth-headless/app/api/auth/resend/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import { scalekit, handleScalekitError } from '@/lib/scalekit';

export async function POST(request: NextRequest) {
try {
const { authReqId } = await request.json();

// Validate auth request ID
if (!authReqId) {
return NextResponse.json(
{ error: 'Auth request ID is required' },
{ status: 400 }
);
}

// Resend the verification code
await scalekit.passwordless.resendPasswordlessEmail(authReqId);

return NextResponse.json({
success: true,
message: 'Verification code resent',
});
} catch (error: any) {
// Handle Scalekit errors gracefully
const { error: errorMessage, status } = handleScalekitError(
error,
'resend code'
);
return NextResponse.json({ error: errorMessage }, { status });
}
}
73 changes: 73 additions & 0 deletions passwordless-auth-headless/app/api/auth/verify-magic-link/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from 'next/server';
import { scalekit, handleScalekitError } from '@/lib/scalekit';

export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const linkToken = searchParams.get('link_token');

// Get auth request ID from cookie (set during initiation)
const authReqId = request.cookies.get('auth-request-id')?.value;

// Validate link token
if (!linkToken) {
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=Invalid magic link`
);
}

// Validate auth request ID (required for Scalekit)
if (!authReqId) {
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=Invalid magic link - please try again`
);
}

// Verify the magic link token
const result = await scalekit.passwordless.verifyPasswordlessEmail(
{ linkToken },
authReqId
);

// Create user session
const userSession = {
email: result.email,
verified: true,
verifiedAt: new Date().toISOString(),
};

// Create redirect response to dashboard
const response = NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/dashboard`
);

// Set secure session cookie
response.cookies.set('user-session', JSON.stringify(userSession), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600, // 1 hour
});

// Clear the auth request ID cookie
response.cookies.set('auth-request-id', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0, // Expire immediately
});

return response;
} catch (error: any) {
// Handle verification errors
const { error: errorMessage } = handleScalekitError(
error,
'magic link verification'
);
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=${encodeURIComponent(
errorMessage
)}`
);
}
}
Loading