Skip to content

Quickstart: Login with Flowsta

This guide will walk you through integrating "Login with Flowsta" into your application in under 15 minutes.

What You'll Build

A complete OAuth integration with:

  • "Login with Flowsta" button in your frontend
  • OAuth callback handler
  • Token exchange in your backend
  • User profile retrieval

Prerequisites

  • A Flowsta developer account (sign up here)
  • A web application (static HTML or modern framework)

Choose Your Integration Method

Flowsta offers multiple ways to integrate "Sign in with Flowsta" depending on your tech stack:

Best for React, Vue, Qwik, or any app with a build system.

  • ✅ Pre-built components
  • ✅ TypeScript support
  • ✅ Automatic PKCE generation
  • ✅ Framework-specific optimizations

Continue with this guide below

🌐 Vanilla JavaScript (No Build Tools)

Best for static HTML sites or simple JavaScript projects.

  • ✅ No npm required
  • ✅ Works with any (or no) framework
  • ✅ Copy-paste ready examples
  • ✅ CDN or self-hosted buttons

View Vanilla JS Guide

🎨 Direct Button Download

Best for quick prototyping or custom implementations.

  • ✅ Download button images (SVG/PNG)
  • ✅ Hotlink from CDN
  • ✅ Manual OAuth URL construction
  • ✅ Maximum flexibility

View Button Downloads


Step 1: Create Your App

1.1 Register Your Application

  1. Go to dev.flowsta.com/dashboard/apps

  2. Click "Create New App"

  3. Enter your app details:

    • Name: My Awesome App
    • Description: A cool app using Flowsta Auth
  4. Copy your Client ID:

    Client ID: flowsta_app_abc123...

No Client Secret Needed

Flowsta uses OAuth 2.0 with PKCE, which means you don't need a client secret for browser or mobile apps. Your Client ID is all you need!

1.2 Configure OAuth Settings

In your app's details page, scroll to "OAuth Settings" and click "Edit Settings":

Add Redirect URIs

Add the URLs where users will be redirected after login:

http://localhost:3000/auth/callback    (for development)
https://yourapp.com/auth/callback       (for production)

Multiple Redirect URIs

You can add up to 10 redirect URIs per app. This allows you to use different URLs for development, staging, and production.

Select Scopes

Choose which data your app needs:

  • profile - User ID, display name, username, DID, agent public key, profile picture (always available)
  • email - Email address and verification status (requires user permission)

Email Scope

The email scope requires explicit user permission. Users will see a consent screen and must approve email access through their Flowsta account settings.

Add Branding (Optional)

Make your consent screen look professional:

  • Logo URL: https://yourapp.com/logo.png (recommended: 200x200px)
  • Privacy Policy: https://yourapp.com/privacy
  • Terms of Service: https://yourapp.com/terms

Click "Save OAuth Settings".

Step 2: Install the Button Widget

The @flowsta/login-button package provides pre-built login buttons for React, Vue, Qwik, and Vanilla JS.

bash
npm install @flowsta/login-button

Framework Support

  • ✅ React 16.8+ (hooks)
  • ✅ Vue 3 (composition API)
  • ✅ Qwik (resumable)
  • ✅ Vanilla JavaScript (any framework or no framework)

Step 3: Add the Login Button

Choose your framework:

tsx
// src/components/LoginButton.tsx
import { FlowstaLoginButton } from '@flowsta/login-button/react';

export function LoginButton() {
  const handleSuccess = (data) => {
    // data.code = authorization code
    // data.state = your state parameter (for CSRF protection)
    console.log('Authorization code:', data.code);
    
    // Send to your backend for token exchange
    window.location.href = `/auth/callback?code=${data.code}&state=${data.state}`;
  };

  const handleError = (error) => {
    console.error('Login error:', error);
    alert(`Login failed: ${error.message}`);
  };

  return (
    <FlowstaLoginButton
      clientId="your_client_id"
      redirectUri="http://localhost:3000/auth/callback"
      scope="profile email"
      variant="dark_pill"
      onSuccess={handleSuccess}
      onError={handleError}
    />
  );
}
vue
<!-- src/components/LoginButton.vue -->
<script setup lang="ts">
import { FlowstaLoginButtonVue } from '@flowsta/login-button/vue';

const handleSuccess = (data: { code: string; state: string }) => {
  console.log('Authorization code:', data.code);
  
  // Send to your backend for token exchange
  window.location.href = `/auth/callback?code=${data.code}&state=${data.state}`;
};

const handleError = (error: Error) => {
  console.error('Login error:', error);
  alert(`Login failed: ${error.message}`);
};
</script>

<template>
  <FlowstaLoginButtonVue
    client-id="your_client_id"
    redirect-uri="http://localhost:3000/auth/callback"
    scope="profile email"
    variant="dark_pill"
    @success="handleSuccess"
    @error="handleError"
  />
</template>
tsx
// src/components/login-button.tsx
import { component$, $ } from '@builder.io/qwik';
import { FlowstaLoginButtonQwik } from '@flowsta/login-button/qwik';

export default component$(() => {
  const handleSuccess = $((data: { code: string; state: string }) => {
    console.log('Authorization code:', data.code);
    
    // Send to your backend for token exchange
    window.location.href = `/auth/callback?code=${data.code}&state=${data.state}`;
  });

  const handleError = $((error: Error) => {
    console.error('Login error:', error);
    alert(`Login failed: ${error.message}`);
  });

  return (
    <FlowstaLoginButtonQwik
      clientId="your_client_id"
      redirectUri="http://localhost:3000/auth/callback"
      scope="profile email"
      variant="dark_pill"
      onSuccess$={handleSuccess}
      onError$={handleError}
    />
  );
});
html
<!-- index.html -->
<div id="login-container"></div>

<script type="module">
  import { FlowstaLoginButton } from '@flowsta/login-button/vanilla';

  const button = new FlowstaLoginButton({
    clientId: 'your_client_id',
    redirectUri: 'http://localhost:3000/auth/callback',
    scope: 'profile email',
    variant: 'dark_pill',
    
    onSuccess: (data) => {
      console.log('Authorization code:', data.code);
      window.location.href = `/auth/callback?code=${data.code}&state=${data.state}`;
    },
    
    onError: (error) => {
      console.error('Login error:', error);
      alert(`Login failed: ${error.message}`);
    },
  });

  button.mount('#login-container');
</script>

Step 4: Create Backend Callback Handler

When Flowsta redirects back to your app, your backend needs to exchange the authorization code for an access token.

javascript
// routes/auth.js
import express from 'express';

const router = express.Router();

router.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state for CSRF protection
  const storedState = req.session.oauthState;
  if (!state || state !== storedState) {
    return res.status(400).send('Invalid state parameter');
  }

  // Retrieve stored code_verifier (from PKCE flow)
  const codeVerifier = req.session.codeVerifier;

  try {
    // Exchange authorization code for tokens
    const tokenResponse = await fetch('https://auth-api.flowsta.com/oauth/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        code,
        redirect_uri: 'http://localhost:3000/auth/callback',
        client_id: process.env.FLOWSTA_CLIENT_ID,
        code_verifier: codeVerifier,  // PKCE - no client_secret needed!
      }),
    });

    if (!tokenResponse.ok) {
      const error = await tokenResponse.json();
      throw new Error(error.error || 'Token exchange failed');
    }

    const { access_token, refresh_token, expires_in } = await tokenResponse.json();

    // Fetch user profile
    const userResponse = await fetch('https://auth-api.flowsta.com/oauth/userinfo', {
      headers: {
        'Authorization': `Bearer ${access_token}`,
      },
    });

    const user = await userResponse.json();

    // Store tokens in session
    req.session.accessToken = access_token;
    req.session.refreshToken = refresh_token;
    req.session.user = user;

    // Redirect to app homepage
    res.redirect('/dashboard');
  } catch (error) {
    console.error('OAuth callback error:', error);
    res.status(500).send('Authentication failed');
  }
});

export default router;
javascript
// app/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const code = searchParams.get('code');
  const state = searchParams.get('state');

  // Verify state
  const cookieStore = cookies();
  const storedState = cookieStore.get('oauth_state')?.value;
  const codeVerifier = cookieStore.get('code_verifier')?.value;

  if (!state || state !== storedState) {
    return NextResponse.json({ error: 'Invalid state' }, { status: 400 });
  }

  try {
    // Exchange code for tokens
    const tokenResponse = await fetch('https://auth-api.flowsta.com/oauth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        code,
        redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`,
        client_id: process.env.FLOWSTA_CLIENT_ID!,
        code_verifier: codeVerifier,  // PKCE - no client_secret needed!
      }),
    });

    const { access_token, refresh_token } = await tokenResponse.json();

    // Fetch user info
    const userResponse = await fetch('https://auth-api.flowsta.com/oauth/userinfo', {
      headers: { 'Authorization': `Bearer ${access_token}` },
    });

    const user = await userResponse.json();

    // Set secure cookies
    const response = NextResponse.redirect(new URL('/dashboard', request.url));
    response.cookies.set('access_token', access_token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 60 * 60 * 24 * 7, // 7 days
    });

    return response;
  } catch (error) {
    console.error('OAuth error:', error);
    return NextResponse.redirect(new URL('/login?error=auth_failed', request.url));
  }
}

Step 5: Store Credentials Securely

Create a .env file in your project root:

bash
# .env
FLOWSTA_CLIENT_ID=your_client_id_here

Load environment variables in your app:

javascript
// Node.js
import dotenv from 'dotenv';
dotenv.config();

const clientId = process.env.FLOWSTA_CLIENT_ID;

PKCE Security

With PKCE, your Client ID can safely be in frontend code - there's no secret to protect. The code_verifier generated per-session provides security.

Step 6: Test Your Integration

6.1 Start Your App

bash
npm run dev

Visit http://localhost:3000

6.2 Click "Login with Flowsta"

You'll be redirected to login.flowsta.com

6.3 Login or Sign Up

  • Existing users: Enter email + password
  • New users: Create a Flowsta account

See which data your app is requesting:

  • ✅ Profile information
  • ✅ Email address (if requested)

6.5 Approve Access

Click "Authorize" to grant access

6.6 Return to Your App

You'll be redirected back with an authorization code, which your backend exchanges for an access token.

6.7 You're Logged In! 🎉

Check your console logs to see the user data:

json
{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "name": "John Doe",
  "preferred_username": "johndoe",
  "did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "agent_pub_key": "uhCAk...",
  "profile_picture": "https://...",
  "email": "john@example.com",
  "email_verified": true
}

Step 7: Handle User Sessions

Store User in Session

javascript
// After successful token exchange
req.session.user = {
  id: user.userId,
  name: user.displayName,
  email: user.email,
  did: user.did,
};

Protect Routes

javascript
// Middleware
function requireAuth(req, res, next) {
  if (!req.session.user) {
    return res.redirect('/login');
  }
  next();
}

// Protected route
app.get('/dashboard', requireAuth, (req, res) => {
  res.render('dashboard', { user: req.session.user });
});

Logout

javascript
app.post('/logout', async (req, res) => {
  // Revoke refresh token
  if (req.session.refreshToken) {
    await fetch('https://auth-api.flowsta.com/oauth/revoke', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Client-Id': process.env.FLOWSTA_CLIENT_ID,
      },
      body: JSON.stringify({
        token: req.session.refreshToken,
        token_type_hint: 'refresh_token',
      }),
    });
  }

  // Clear session
  req.session.destroy();
  res.redirect('/');
});

Next Steps

📚 Deep Dive

🎨 Customization

  • Try different button variants: light_pill, neutral_rectangle, etc.
  • Add your app's logo to the consent screen
  • Customize redirect behavior

🔐 Production Checklist

  • [ ] Use HTTPS for all redirect URIs
  • [ ] Implement CSRF protection with state parameter (SDK handles this automatically)
  • [ ] Use secure session storage (httpOnly cookies recommended)
  • [ ] Implement token refresh logic
  • [ ] Add error handling and user feedback
  • [ ] Test with multiple user accounts
  • [ ] Monitor OAuth audit logs in developer dashboard

No Client Secrets

With PKCE, you don't need to manage client secrets! Your Client ID can safely be in frontend code. The code_verifier provides security without secrets.

Common Issues

"Invalid redirect_uri" Error

Cause: The redirect URI doesn't match any URI configured in your app settings.

Solution: Go to your app settings and add the exact redirect URI (including protocol and port).

"Invalid client" Error

Cause: Client ID is incorrect.

Solution: Double-check your Client ID in the .env file or Developer Dashboard.

"Code has expired" Error

Cause: Authorization codes expire after 10 minutes.

Solution: Ensure your backend exchanges the code immediately after receiving it.

Email Not Returned

Cause: User denied the email scope or your app doesn't have email scope configured.

Solution:

  1. Ensure your app has the email scope enabled in OAuth settings
  2. User must click "Authorize" on the consent screen to grant email access
  3. If user previously denied access, they'll need to re-authorize your app

Need Help?

Documentation licensed under CC BY-SA 4.0.