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
1 change: 1 addition & 0 deletions docs/API-Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ These JS APIs falls into the most common category and almost all apps, component

- [Audio](API/fliplet-audio.md) (`fliplet-audio`)
- [Audio Player](API/fliplet-audio-player.md) (`fliplet-audio-player`)
- [Auth](API/fliplet-auth.md) (`fliplet-auth`)
- [Barcode](API/fliplet-barcode.md) (`fliplet-barcode`)
- [Communicate](API/fliplet-communicate.md) (`fliplet-communicate`)
- [Content](API/fliplet-content.md) (`fliplet-content`)
Expand Down
216 changes: 216 additions & 0 deletions docs/API/fliplet-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
---
description: Sign users into your Fliplet app with one line of code — email/password or SSO (Google, Microsoft, Apple).
---

# Auth JS APIs

The Fliplet Auth JS APIs sign users into your Fliplet app with their existing Fliplet account. One call opens a popup, handles the authentication round-trip, and resolves with the signed-in user. Email/password and SSO (Google, Microsoft, Apple) are supported out of the box.

Use these APIs to authenticate Fliplet users, read the signed-in user in your app code, sign users out, and get the auth token for custom API requests.

## Before you start

Add the `fliplet-auth` package to your app's dependencies. This exposes `Fliplet.Auth.*` at runtime.

<p class="warning"><code>Fliplet.Auth</code> authenticates <strong>existing Fliplet user accounts</strong>. It does not create new accounts from inside an app — only Fliplet Studio's sign-up page can do that. A user who tries to sign in with an SSO provider (Google, Microsoft, Apple) that isn't linked to any Fliplet account sees an error asking them to sign up at Fliplet Studio first. Direct users to Fliplet Studio to create their account, then back to your app to sign in.</p>

`Fliplet.Auth` is the high-level API for signing users in and out — prefer it for app code. It is distinct from two other APIs that share related concepts:

- **`Fliplet.User`** — low-level primitives (`getAuthToken`, `setAuthToken`, `getCachedSession`). `Fliplet.Auth` uses these internally; you rarely need to call them directly.
- **Data source login component** — an app-level authentication system that uses a data source as the user table. It's a separate system from `Fliplet.Auth` and is appropriate when you want users stored in your app's own data source rather than in Fliplet's user database. See [data source login](components/login.md) for that.

## Methods

### Sign a user in

`Fliplet.Auth.signIn()` opens the unified sign-in popup and resolves with `{ user, token }` when the user successfully signs in.

```js
Fliplet.Auth.signIn().then(function(result) {
// result.user: { id, email, firstName, lastName, userRoleId }
// result.token: auth token string, e.g. "eu--55a54008...-203-8965"
console.log('Signed in as', result.user.email);
}).catch(function(err) {
// surface the error to the user and let them retry
console.error(err.message);
});
```

Pre-fill the email field in the sign-in form by passing `options.email`:

```js
Fliplet.Auth.signIn({ email: 'alice@acme.com' });
```

The promise **rejects with an `Error`** when:

- The popup is blocked by the browser → `Error('Sign-in popup was blocked...')`
- The user closes the popup without completing sign-in → `Error('Sign-in was cancelled.')`
- Sign-in times out after 10 minutes → `Error('Sign-in timed out. Please try again.')`
- The API returns a sign-in error → `Error(<message from API>)`

Always handle rejection in your app code — the examples on this page consistently do so.

### Get the currently signed-in user

`Fliplet.Auth.currentUser()` resolves to the signed-in user, or `null` when no user is signed in.

```js
Fliplet.Auth.currentUser().then(function(user) {
if (!user) {
// not signed in
return;
}

// user shape: { id, email, userRoleId, region, legacy }
console.log(user.email);
});
```

The user object returned by `currentUser()` contains the fields that persist locally between sessions: `id`, `email`, `userRoleId`, `region`, and `legacy`. It does **not** include `firstName` / `lastName` (which `signIn()` returns) — to access those, either store them yourself after `signIn()` resolves, or query `v1/user` with `Fliplet.Auth.getToken()`.

### Check whether a user is signed in

`Fliplet.Auth.isSignedIn()` resolves to `true` or `false` — a convenience helper on top of `currentUser()`.

```js
Fliplet.Auth.isSignedIn().then(function(isSignedIn) {
if (isSignedIn) {
// show authenticated UI
}
});
```

### Get the user's auth token

`Fliplet.Auth.getToken()` resolves to the signed-in user's auth token, or `null` when no user is signed in. Use it to authenticate calls to the Fliplet API as the signed-in user.

<p class="warning"><strong>Only send the auth token to the Fliplet API.</strong> The token grants full access to this user's Fliplet account — treat it like a password. Never put it in the URL, log it, or send it to a third-party / non-Fliplet endpoint, as that hands the user's account to whoever receives it. To call your own backend, send a short-lived token your backend issues, not the Fliplet auth token.</p>

```js
Fliplet.Auth.getToken().then(function(token) {
if (!token) {
throw new Error('Not signed in');
}

return Fliplet.API.request({
url: 'v1/user',
headers: { 'Auth-Token': token }
});
}).then(function(response) {
console.log(response.user);
});
```

`Fliplet.API.request()` is the recommended way to call the Fliplet API — it targets the correct API host for you and keeps the token on the Fliplet origin.

### Sign the user out

`Fliplet.Auth.signOut()` clears locally stored credentials and invalidates the server-side session.

```js
Fliplet.Auth.signOut().then(function() {
// user is signed out; currentUser() now resolves to null
});
```

If the server-side logout request fails (for example, the device is offline), local credentials are still cleared so the user is effectively signed out.

### Listen for sign-in / sign-out events

`Fliplet.Auth.onChange(callback)` subscribes to auth state changes. The callback fires with the new user after a successful sign-in, or with `null` after sign-out completes. Returns an unsubscribe function.

```js
var unsubscribe = Fliplet.Auth.onChange(function(user) {
if (user) {
// user just signed in — refresh the UI as this user
} else {
// user just signed out — reset to logged-out state
}
});

// Stop listening when no longer needed
unsubscribe();
```

<p class="quote"><code>onChange</code> fires only for sign-ins and sign-outs performed through <code>Fliplet.Auth.signIn()</code> and <code>Fliplet.Auth.signOut()</code>. It does not currently fire for auth changes made by other mechanisms (for example a Fliplet Login component on another screen), since those don't route through this API.</p>

## Examples

### Gate a screen behind sign-in

Prompt the user to sign in if they're not already signed in, then render the screen.

```js
Fliplet.Auth.currentUser().then(function(user) {
if (user) {
return user;
}

return Fliplet.Auth.signIn().then(function(result) {
return result.user;
});
}).then(function(user) {
// render the screen as this user
document.querySelector('.greeting').textContent = 'Welcome, ' + user.email;
}).catch(function(err) {
// user cancelled sign-in or it failed
console.error(err.message);
});
```

### Show the signed-in user's name in the header

```js
Fliplet.Auth.currentUser().then(function(user) {
var greeting = user
? 'Signed in as ' + user.email
: 'Not signed in';

document.querySelector('.profile-greeting').textContent = greeting;
});
```

### Sign-out button

```js
document.querySelector('.sign-out-button').addEventListener('click', function() {
Fliplet.Auth.signOut().then(function() {
location.reload();
});
});
```

### Refresh the UI when auth state changes

```js
Fliplet.Auth.onChange(function(user) {
if (user) {
document.body.classList.add('signed-in');
} else {
document.body.classList.remove('signed-in');
}
});
```

### Make an authenticated API request

```js
Fliplet.Auth.getToken().then(function(token) {
if (!token) {
throw new Error('Not signed in');
}

return Fliplet.API.request({
url: 'v1/organizations',
headers: { 'Auth-Token': token }
});
}).then(function(response) {
console.log('Organizations:', response.organizations);
}).catch(function(err) {
console.error(err);
});
```

[Back to API documentation](../API-Documentation.md)
{: .buttons}
1 change: 1 addition & 0 deletions docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
<li><a href="/API-Documentation.html">Overview</a></li>
<li><p>Libraries</p></li>
<li><a href="/API/core/overview.html">Core</a></a></li>
<li><a href="/API/fliplet-auth.html">Auth <span class="label">NEW</span></a></li>
<li><a href="/API/fliplet-communicate.html">Communicate</a></li>
<li><a href="/API/fliplet-datasources.html">Data Sources</a></li>
<li><a href="/API/fliplet-encryption.html">Encryption</a></li>
Expand Down
73 changes: 58 additions & 15 deletions fliplet-login.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require('colors');

const _ = require('lodash');
const url = require('url');

const auth = require('./lib/auth');
const config = require('./lib/config');
Expand All @@ -20,57 +20,100 @@ ${'Press Ctrl+C to exit.'.blue}

const port = 9001;
const baseUrl = `http://localhost:${port}`;
const redirectUrl = `${config.api_url}v1/auth/third-party?redirect=${encodeURIComponent(`${baseUrl}/callback`)}&responseType=code&source=CLI&title=Sign%20in%20to%20Authorize%20the%20Fliplet%20CLI`;
const callbackUrl = `${baseUrl}/callback`;

// New unified sign-in URL — replaces the legacy /v1/auth/third-party flow.
// The page validates the callback URL against an allow-list (which already
// includes http://localhost:9001/callback) and on success navigates the
// browser to <callback>?token=XXX&user=<url-encoded-json>.
//
// No `&source=CLI`: the unified login page derives the session source from
// `isEmbedded` (views/login.pug) and does not read `?source=`, so passing it
// here would be a dead param that never tags the session.
const redirectUrl = `${config.api_url}v1/auth/login`
+ `?return=callback`
+ `&callback=${encodeURIComponent(callbackUrl)}`;

require('http').createServer(function(req, res) {
if (req.url.match(/login/)) {
// Route on the parsed pathname, NOT a loose `req.url.match(/.../)`. The
// unified callback carries the signed-in user's data in the query string
// (`?token=...&user=<url-encoded-json>`), so a substring match against the
// whole URL would misroute any user whose email or name contains "login"
// or "success" (e.g. john.login@acme.com, anyone named "Success") into the
// wrong branch — breaking their sign-in. Pathname matching is exact.
const parsed = url.parse(req.url, true);
const pathname = parsed.pathname;

if (pathname === '/login') {
res.writeHead(302, {
'Location': redirectUrl
});

return res.end();
}

if (req.url.match(/success/)) {
if (pathname === '/success') {
res.end(`<html><body style="font-size:20px;font-family:sans-serif"><p><strong>You have been logged in successfully to your Fliplet account.</strong></p><p>You may now <a href="javascript:window.close()">close</a> this window now and return to the terminal to continue the process.</p><script>setTimeout(function () { window.close(); }, ${config.env === 'local' ? 0 : 5000});</script></body></html>`);

return process.exit();
}

if (req.url.match(/callback/)) {
const authToken = _.last(req.url.split('auth_token='));
if (pathname === '/callback') {
// Token (and optional user payload) come from the callback query string.
// The unified contract delivers them as `?token=...&user=<url-encoded-json>`.
const authToken = parsed.query.token;

if (!authToken) {
console.error('No auth token received from the sign-in flow.');
res.writeHead(400);
res.end('Missing token');
return process.exit(1);
}

auth.setUserForToken(authToken).then(function(user) {
organizations.getOrganizationsList().then(function onGetOrganizations(organizations) {
if (!organizations.length) {
return console.error('Your organization has not been found.');
}

if (organizations.length > 1) {
return console.error('You belong to multiple organizations.');
console.error('Your organization has not been found.');
res.writeHead(400);
res.end('No organizations available for this account.');
return process.exit(1);
}

// For users belonging to multiple organizations, default to the
// first one. They can switch later via `fliplet organization <id>`
// (use `fliplet list-organizations` to see all available IDs).
const userOrganization = organizations[0];

config.set('organization', userOrganization);

console.log('----------------------------------------------------------\r\n');
console.log(`You have logged in successfully. Welcome back, ${user.fullName.yellow.underline}!`);
console.log(`Your organization has been set to ${userOrganization.name.green.underline} (#${userOrganization.id}). Your account email is ${user.email.yellow.underline}.`);
console.log(`You can now develop and publish components via the ${'fliplet run'.bgBlack.red} command!
`);

if (organizations.length > 1) {
console.log(`\n${'Note'.yellow}: you belong to ${organizations.length} organizations. To switch, run ${'fliplet list-organizations'.bgBlack.red} to see all of them and ${'fliplet organization <id>'.bgBlack.red} to change.`);
}

console.log(`\nYou can now develop and publish components via the ${'fliplet run'.bgBlack.red} command!\n`);

res.writeHead(302, {
'Location': `${baseUrl}/success?${Date.now()}`
});

return res.end();
}).catch(function(err) {
console.error('Failed to fetch organizations:', err);
res.writeHead(500);
res.end('Failed to fetch organizations');
return process.exit(1);
});
}).catch(function(err) {
console.error(err);
process.exit();
res.writeHead(500);
res.end('Authentication failed');
return process.exit(1);
});
}
}).listen(9001);
}).listen(port);

require('openurl').open(`${baseUrl}/login?${Date.now()}`);
6 changes: 4 additions & 2 deletions lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ function login(options) {
function setUserForToken(authToken) {
return new Promise(function(resolve, reject) {
request({
url: config.api_url + 'v1/user?auth_token=' + authToken
url: config.api_url + 'v1/user',
headers: { 'Auth-Token': authToken }
}, function(error, response, body) {
if (error) {
return reject({ error, response, body });
Expand Down Expand Up @@ -64,7 +65,8 @@ function isAdmin() {
return new Promise(function(resolve, reject) {
request({
method: 'GET',
url: `${config.api_url}v1/user?auth_token=${auth_token}`
url: `${config.api_url}v1/user`,
headers: { 'Auth-Token': auth_token }
}, function(error, response, body) {
if (error) {
reject(error);
Expand Down
Loading