FMLDevelopers

Sign in with FML

With “Sign in with FML”, your app gets a member's token without ever seeing their password: the member signs in on fmylife.com and agrees to let your app act on their behalf. It's OAuth 2 (authorization code with PKCE), like “Sign in with Google”. For an app meant for people other than you, use it instead of signing in with a password.

1. Turn it on for your key

In My keys, turn on “Sign in with FML” on your app's key and enter its redirect URIs (redirect_uri): https, http://localhost for development, or a mobile app scheme like com.myapp:/oauth. You get a client_id and, for an app with a server, a client_secret, shown once. A mobile or JavaScript app can't keep a secret: check the no-secret option, PKCE is enough.

2. Send the member to FML

Create a random code_verifier (43 to 128 characters) and a state, keep them for the next step, then open this URL in the browser. The code_challenge is the verifier's SHA-256, base64url-encoded. The redirect_uri must be exactly one of the registered URIs.

URL
https://www.fmylife.com/oauth/authorize
  ?response_type=code
  &client_id=$CLIENT_ID
  &redirect_uri=https%3A%2F%2Fmyapp.com%2Fauth%2Fcallback
  &state=$STATE
  &code_challenge=$CODE_CHALLENGE
  &code_challenge_method=S256

On mobile, open it in the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android), never in a WebView: the member must see they're really on fmylife.com.

3. Get the code

The member signs in if needed, then accepts or declines. FML sends them back to your redirect_uri with the same state: check it first.

URL
https://myapp.com/auth/callback?code=…&state=$STATE
https://myapp.com/auth/callback?error=access_denied&state=$STATE

4. Exchange the code for a token

The code is valid for 10 minutes and works once. Send it as a form with the verifier. client_secret is required for an app with a server, and left out for an app without secret (you can also send it with HTTP Basic).

Shell
curl "https://www.fmylife.com/oauth/token" \
  -d grant_type=authorization_code \
  -d code=$CODE \
  -d redirect_uri=https%3A%2F%2Fmyapp.com%2Fauth%2Fcallback \
  -d client_id=$CLIENT_ID \
  -d client_secret=$CLIENT_SECRET \
  -d code_verifier=$CODE_VERIFIER

The response has the member's token and identity:

JSON
{
  "access_token": "3f1c9b0e8d7a6f5e4d3c2b1a09f8e7d6",
  "token_type": "Bearer",
  "scope": "api",
  "user": { "id": 123456, "username": "Name" }
}

5. Call the API on the member's behalf

Send the token in Authorization: Bearer (or X-VDM-Api-Token) along with your key: it only works with it. Every endpoint of the reference marked “Member token required” is then available.

Shell
curl "https://www.fmylife.com/api/v2/user/123456/favorites" \
  -H "X-VDM-Api-Key: $API_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Lifetime and revocation

  • The token doesn't expire: store it safely (device keychain, encrypted database), like a password.
  • It stops working if the member signs out of your app (GET /user/logout), changes their password, or if you revoke your key.
  • A 401 on an endpoint that needs a token: send the member back to step 2.

Errors

/oauth/token answers in the OAuth 2 format: {"error": "…", "error_description": "…"}. invalid_client (401): wrong client_id or client_secret, or revoked key. invalid_grant (400): expired or used code, or one that doesn't match the verifier or the redirect_uri. On /oauth/authorize, an unknown client_id or redirect_uri is shown to the member instead of sending them back to you.

Full example (Node.js)

JavaScript
import { createHash, randomBytes } from "node:crypto";

// 1. Before redirecting the member: a verifier and a state, kept in the session
const verifier = randomBytes(32).toString("base64url");
const state = randomBytes(16).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

const url = new URL("https://www.fmylife.com/oauth/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: process.env.CLIENT_ID,
  redirect_uri: "https://myapp.com/auth/callback",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
}).toString();
// redirect the browser to url

// 2. On redirect_uri: check the state, then exchange the code
const response = await fetch("https://www.fmylife.com/oauth/token", {
  method: "POST",
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code,
    redirect_uri: "https://myapp.com/auth/callback",
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET, // not for an app without secret
    code_verifier: verifier,
  }),
});
const { access_token, user } = await response.json();