Social login is expected on every modern web application. Users want to click "Continue with Google" and be authenticated in seconds, without creating another password. Developers want to offer this, but the implementation is notoriously complex: OAuth2 authorization codes, redirect URIs, state parameters, PKCE code verifiers, token exchanges, and provider-specific quirks.
In a Node.js application, implementing Google OAuth requires passport, passport-google-oauth20, session configuration, callback routes, and provider-specific configuration. GitHub requires passport-github2 with different scopes and different user profile fields. Each provider is a separate package with its own API.
FLIN provides built-in functions for six OAuth providers: Google, GitHub, Discord, Apple, LinkedIn, and Telegram. Each provider follows the same three-step pattern: generate the auth URL, handle the callback, and create or log in the user.
The Universal Pattern
Every OAuth provider in FLIN follows the same flow:
Step 1: Generate auth URL (login page)
auth = auth_PROVIDER_login(baseUrl)
session.PROVIDERState = auth.state
<a href={auth.url}>Continue with PROVIDER</a>
Step 2: Handle callback (callback page)
result = auth_PROVIDER_callback(query.code, query.state, session.PROVIDERState)
session.PROVIDERState = none
Step 3: Find or create user
if result.ok {
existing = User.where(email == result.user.email).first
if existing != none { log in }
else { create new user }
}The function names, the session storage pattern, and the find-or-create logic are identical across providers. Learn one, and you know them all.
Google OAuth with PKCE
Google is the most commonly used OAuth provider. FLIN implements it with PKCE (Proof Key for Code Exchange), the recommended security extension for public clients:
flin// app/login.flin -- Step 1: Generate auth URL
baseUrl = env("BASE_URL") || "http://localhost:3000"
googleAuth = auth_google_login(baseUrl)
// Store state and PKCE verifier in session
session.googleState = googleAuth.state
session.googleRedirectUri = googleAuth.redirect_uri
session.googleCodeVerifier = googleAuth.code_verifier
// Render the login button
<a href={googleAuth.url} class="social-btn">
Continue with Google
</a>The auth_google_login() function generates:
- A random state parameter (CSRF protection)
- A code_verifier and code_challenge (PKCE)
- The complete authorization URL with scopes, redirect URI, and response type
flin// app/auth/google/callback.flin -- Step 2: Handle callback
layout = "auth"
result = auth_google_callback(query.code, query.state, session.googleState)
session.googleState = none
authOk = false
fn processGoogleAuth() {
if result.ok {
existing = User.where(email == result.user.email && role == "User").first
if existing != none {
session.user = existing.email
session.userName = existing.name || result.user.name
session.userId = to_text(existing.id)
} else {
newUser = User {
email: result.user.email,
name: result.user.name,
provider: "Google",
providerId: to_text(result.user.id),
avatar: result.user.avatar || "",
emailVerified: true
}
save newUser
session.user = newUser.email
session.userName = newUser.name
session.userId = to_text(newUser.id)
}
authOk = true
}
}
processGoogleAuth()
{if authOk}
<h2>Welcome!</h2>
<script>setTimeout(function() { window.location.href = "/tasks"; }, 1000);</script>
{else}
<h2>Authentication failed</h2>
<a href="/login">Try again</a>
{/if}GitHub OAuth
GitHub follows the same pattern with two key differences: private email handling and username availability.
flin// app/login.flin
githubAuth = auth_github_login(baseUrl)
session.githubState = githubAuth.state
session.githubRedirectUri = githubAuth.redirect_uri
session.githubCodeVerifier = githubAuth.code_verifier
<a href={githubAuth.url} class="social-btn">
Continue with GitHub
</a>GitHub users can hide their email address. The callback must handle this:
flin// app/auth/github/callback.flin
result = auth_github_callback(query.code, query.state, session.githubState)
session.githubState = none
fn processGithubAuth() {
if result.ok {
// Handle private email
githubEmail = result.user.email || to_text(result.user.id) + "@github.flin"
githubName = result.user.name || result.user.login || "GitHub User"
existing = User.where(email == githubEmail && role == "User").first
if existing != none {
session.user = existing.email
session.userName = existing.name || githubName
session.userId = to_text(existing.id)
} else {
newUser = User {
email: githubEmail,
name: githubName,
provider: "GitHub",
providerId: to_text(result.user.id),
avatar: result.user.avatar_url || "",
emailVerified: result.user.email != none
}
save newUser
session.user = newUser.email
session.userName = newUser.name
session.userId = to_text(newUser.id)
}
authOk = true
}
}The fallback email {id}@github.flin ensures that users without a public email can still register. The emailVerified flag is set based on whether we got a real email from GitHub.
Discord OAuth
Discord uses standard OAuth2 with the identify email scopes:
flin// Setup
discordAuth = auth_discord_login(baseUrl)
session.discordState = discordAuth.state
session.discordRedirectUri = discordAuth.redirect_uri
<a href={discordAuth.url} class="social-btn">
Continue with Discord
</a>
// Callback
result = auth_discord_callback(query.code, query.state, session.discordState)
session.discordState = none
// ... same find-or-create patternApple Sign-In
Apple Sign-In has unique requirements: it sends user data only on the first authentication, uses a POST callback instead of GET, and requires a client secret generated from a private key.
flin// Setup
appleAuth = auth_apple_login(baseUrl)
session.appleState = appleAuth.state
<a href={appleAuth.url} class="social-btn">
Continue with Apple
</a>
// Callback (handles POST from Apple)
result = auth_apple_callback(body.code, body.state, session.appleState)FLIN handles Apple's quirks internally. The developer uses the same pattern as every other provider.
LinkedIn OAuth
LinkedIn uses OAuth 2.0 with the openid profile email scopes:
flinlinkedinAuth = auth_linkedin_login(baseUrl)
session.linkedinState = linkedinAuth.state
<a href={linkedinAuth.url} class="social-btn">
Continue with LinkedIn
</a>
// Callback
result = auth_linkedin_callback(query.code, query.state, session.linkedinState)Telegram Login Widget
Telegram uses a different mechanism: a JavaScript login widget that sends a signed hash to your callback URL. FLIN verifies the hash using your bot token:
flin// Callback (Telegram sends data as query parameters)
result = auth_telegram_callback(query)
fn processTelegramAuth() {
if result.ok {
telegramEmail = result.user.email || to_text(result.user.id) + "@telegram.flin"
// ... same find-or-create pattern
}
}The OAuth Result Object
Every auth_*_callback() function returns the same structure:
flin// Success
result.ok // true
result.user.id // Provider's user ID
result.user.email // Email (may be none for some providers)
result.user.name // Display name
result.user.avatar // Profile picture URL
result.user.provider // "google", "github", etc.
// Failure
result.ok // false
result.error // Error descriptionThis consistent interface means the find-or-create logic is identical for every provider. The only differences are in the session variable names and the login page setup.
Environment Variables
Each provider requires credentials from their developer console:
# .env
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxx
GITHUB_CLIENT_ID=xxx
GITHUB_CLIENT_SECRET=xxx
DISCORD_CLIENT_ID=xxx
DISCORD_CLIENT_SECRET=xxx
APPLE_CLIENT_ID=com.example.app
APPLE_TEAM_ID=xxx
APPLE_KEY_ID=xxx
APPLE_PRIVATE_KEY_PATH=.secrets/apple-auth-key.p8
LINKEDIN_CLIENT_ID=xxx
LINKEDIN_CLIENT_SECRET=xxx
TELEGRAM_BOT_TOKEN=xxx:xxxIf a provider's credentials are not configured, the auth_*_login() function returns none and the login button should be hidden:
flingoogleAuth = auth_google_login(baseUrl)
{if googleAuth != none}
<a href={googleAuth.url}>Continue with Google</a>
{/if}Security: State and PKCE
Every OAuth flow in FLIN is protected by two mechanisms:
State parameter. A random string stored in the session and included in the authorization URL. The callback verifies that the state in the response matches the state in the session. This prevents CSRF attacks where an attacker tricks a user into authenticating with the attacker's account.
PKCE (Proof Key for Code Exchange). A random code verifier is generated and its SHA-256 hash (the code challenge) is sent with the authorization request. When exchanging the authorization code for tokens, the original code verifier is sent. The authorization server verifies that the hash matches. This prevents authorization code interception attacks.
rustfn generate_pkce() -> (String, String) {
let verifier = generate_random_string(64);
let challenge = base64url_encode(&sha256(verifier.as_bytes()));
(verifier, challenge)
}Both protections are generated and verified automatically by the auth_<em>_login() and auth_</em>_callback() functions. The developer stores the state and verifier in the session but never needs to understand why.
Why Built-In OAuth Matters
OAuth2 is a standard, but the implementation details vary wildly between providers. Google requires PKCE. Apple sends a POST. GitHub might not return an email. Discord uses different scope names. LinkedIn has deprecated multiple API versions.
By absorbing these differences into built-in functions, FLIN ensures that: 1. Every OAuth implementation follows the standard correctly. 2. Security measures (state, PKCE) are always applied. 3. Provider quirks are handled internally. 4. The developer interface is consistent across all providers.
The alternative -- installing a separate library for each provider, configuring each one differently, and hoping they all handle security correctly -- is exactly the kind of complexity that FLIN was designed to eliminate.
In the next article, we cover WhatsApp OTP authentication -- the authentication method designed specifically for the African market where phone-based identity is more common than email.
This is Part 111 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.
Series Navigation: - [110] Two-Factor Authentication (TOTP) - [111] OAuth2 and Social Authentication (you are here) - [112] WhatsApp OTP Authentication for Africa - [113] Request Body Validators