Skip to content
Back to skills

Stacks Socials

ASecurity

Use when implementing social authentication in Stacks - OAuth2 flows with GitHub/Google/Facebook/Twitter providers, the AbstractProvider base class, PKCE support, state management, scope configuration, social user profiles, or token handling. Covers @stacksjs/socials.

  • 631 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added June 5, 2026
developmenttypescriptgobashnodegitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 3, 2026

npx -y skills add stacksjs/stacks --skill stacks-socials --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Stacks Socials?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Stacks Socials
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/stacksjs-stacks-socials/badge)](https://www.skillsdirectory.com/skills/stacksjs-stacks-socials)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: stacks-socials
description: Use when implementing social authentication in Stacks - OAuth2 flows with GitHub/Google/Facebook/Twitter providers, the AbstractProvider base class, PKCE support, state management, scope configuration, social user profiles, or token handling. Covers @stacksjs/socials.
license: MIT
compatibility: Bun >= 1.3.0, TypeScript
allowed-tools: Read Edit Write Bash Grep Glob
---

# Stacks Socials

OAuth2-based social authentication with 4 built-in providers.

## Key Paths
- Core package: `storage/framework/core/socials/src/`

## Source Files
```
socials/src/
├── index.ts          # re-exports from drivers/
├── abstract.ts       # AbstractProvider base class
├── token.ts          # Token class
├── types.ts          # SocialUser, provider-specific interfaces
├── exceptions.ts     # InvalidStateException, ConfigException
└── drivers/
    ├── index.ts      # re-exports all 4 providers
    ├── github.ts     # GitHub OAuth provider
    ├── google.ts     # Google OAuth provider
    ├── facebook.ts   # Facebook OAuth provider
    └── twitter.ts    # Twitter/X OAuth2 + PKCE provider
```

## AbstractProvider Base Class

All providers extend `AbstractProvider` which implements `ProviderInterface`. The constructor takes a `ProviderConfig`:

```typescript
interface ProviderConfig {
  clientId: string
  clientSecret: string
  redirectUrl: string
  guzzle?: Record<string, any>
}
```

Properties and methods on `AbstractProvider`:

```typescript
abstract class AbstractProvider implements ProviderInterface {
  // Protected state
  protected clientId: string
  protected clientSecret: string
  protected redirectUrl: string
  protected parameters: Record<string, any> = {}
  protected _scopes: string[] = []
  protected scopeSeparator: string = ','
  protected _stateless: boolean = false
  protected _usesPKCE: boolean = false
  protected user: SocialUser | null = null

  // Abstract methods (each provider implements)
  abstract getAuthUrl(): Promise<string>           // public, returns Promise
  protected abstract getTokenUrl(): string         // protected
  abstract getAccessToken(code: string): Promise<string>
  abstract getUserByToken(token: string): Promise<SocialUser>

  // Scope management
  scopes(scopes: string | string[]): this      // merge into existing (deduplicates via Set)
  setScopes(scopes: string | string[]): this   // replace all scopes (deduplicates via Set)
  getScopes(): string[]

  // Configuration
  setRedirectUrl(url: string): this
  stateless(): this                             // sets _stateless = true
  enablePKCE(): this                            // sets _usesPKCE = true
  with(parameters: Record<string, any>): this   // replaces custom query parameters

  // User retrieval
  async userFromToken(token: string): Promise<SocialUser>  // calls getUserByToken, adds token

  // URL building (protected)
  protected buildAuthUrlFromBase(url: string, state: string | null): string
  protected getCodeFields(state: string | null = null): Record<string, any>
  protected formatScopes(scopes: string[], scopeSeparator: string): string

  // State helpers (protected)
  protected usesState(): boolean          // returns !_stateless
  protected isStateless(): boolean        // returns _stateless
  protected getState(): string            // 20 random bytes -> 40 hex chars

  // PKCE helpers (protected)
  protected usesPKCE(): boolean
  protected getCodeVerifier(): string              // 48 random bytes -> 96 hex chars
  protected async getCodeChallenge(): Promise<string>  // SHA-256 of verifier, base64url encoded
  protected getCodeChallengeMethod(): string       // always 'S256'
}
```

The `getCodeFields()` method builds the query parameters for the authorization URL:
- Always includes: `client_id`, `redirect_uri`, `scope` (joined by `scopeSeparator`), `response_type: 'code'`
- Conditionally adds `state` when `usesState()` is true
- Conditionally adds `code_challenge` and `code_challenge_method` when `usesPKCE()` is true
- Merges in any custom `parameters` set via `with()`

## Provider Implementations

### GitHub Provider

```typescript
class GitHubProvider extends AbstractProvider {
  protected baseUrl = 'https://github.com'
  protected apiUrl = 'https://api.github.com'
}
```

- Config source: `config.services.github` (clientId, clientSecret, redirectUrl, scopes)
- Default scopes: `['read:user', 'user:email']`
- Auth URL: `https://github.com/login/oauth/authorize` with scopes joined by space
- Token URL: `https://github.com/login/oauth/access_token` (POST)
- User API: Fetches `/user` and `/user/emails` in parallel via `Promise.all`
- Email resolution: Primary email first, then verified, then first available
- Uses `@stacksjs/api` `fetcher` for HTTP requests
- Throws `ConfigException` if clientId, clientSecret, or redirectUrl missing
- GitHub token response: `GitHubTokenResponse { access_token, error?, error_description? }`

### Google Provider

```typescript
class GoogleProvider extends AbstractProvider {
  protected baseUrl = 'https://accounts.google.com'
  protected apiUrl = 'https://www.googleapis.com'
}
```

- Config source: `config.services.google`
- Default scopes: `['https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/userinfo.email']`
- Auth URL: `https://accounts.google.com/o/oauth2/v2/auth` (includes `access_type: 'offline'`, `prompt: 'consent'`)
- Token URL: `https://accounts.google.com/oauth2/v4/token` (POST with `grant_type: 'authorization_code'`)
- User API: `https://www.googleapis.com/oauth2/v2/userinfo` with Bearer token
- Maps `given_name` to `nickname`, `picture` to `avatar`
- Google user type: `{ id, email, verified_email, name, given_name, family_name, picture, locale }`

### Facebook Provider

```typescript
class FacebookProvider extends AbstractProvider {
  protected baseUrl = 'https://www.facebook.com'
  protected apiUrl = 'https://graph.facebook.com'
}
```

- Config source: `config.services.facebook`
- Default scopes: `['email', 'public_profile']`
- Auth URL: `https://www.facebook.com/v18.0/dialog/oauth` with scopes joined by comma
- Token URL: `https://graph.facebook.com/v18.0/oauth/access_token` (GET request, not POST)
- User API: `https://graph.facebook.com/v18.0/me?fields=id,name,email,picture`
- Token is passed as query parameter `access_token`, not in Authorization header
- Always sets `nickname: null` (Facebook does not expose usernames)
- Avatar extracted from `picture.data.url`
- Error response shape: `{ error: { message, type, code } }` (nested object, unlike other providers)

### Twitter/X Provider

```typescript
class TwitterProvider extends AbstractProvider {
  protected baseUrl = 'https://twitter.com'
  protected apiUrl = 'https://api.twitter.com'
  private codeVerifier: string | null = null
}
```

- Config source: `config.services.twitter`
- Default scopes: `['users.read', 'tweet.read']`
- Uses OAuth 2.0 with PKCE (not OAuth 1.0a)
- Auth URL: `https://twitter.com/i/oauth2/authorize` with PKCE code challenge
- Token URL: `https://api.twitter.com/2/oauth2/token` (POST with Basic auth)
- User API: `https://api.twitter.com/2/users/me?user.fields=profile_image_url` with Bearer token
- PKCE implementation uses its own `generateCodeVerifier()` and `generateCodeChallenge()` methods (using `node:crypto` `randomBytes` and `createHash`), not the base class PKCE
- Code verifier: `randomBytes(32)` -> base64 -> alphanumeric only -> max 128 chars
- Token exchange uses Basic auth: `Buffer.from(clientId:clientSecret).toString('base64')`
- `getAccessToken()` throws if `codeVerifier` is null -- `getAuthUrl()` must be called first
- Maps `username` to `nickname`, `profile_image_url` to `avatar`

## SocialUser Interface

```typescript
interface SocialUser {
  id: string
  nickname: string | null
  name: string
  email: string | null
  avatar: string | null
  token: string
  raw?: any               // full provider API response
}
```

## ProviderInterface

```typescript
interface ProviderInterface {
  getAuthUrl: () => Promise<string>
  getAccessToken: (code: string) => Promise<string>
  getUserByToken: (token: string) => Promise<SocialUser>
}
```

## Token Class

```typescript
class Token {
  constructor(
    public accessToken: string,
    public refreshToken: string | null = null,
    public expiresIn: number | null = null,
    public approvedScopes: string[] = [],
  ) {}
}
```

## Provider-Specific Types

```typescript
// GitHub
interface GitHubUser { id: number, login: string, name: string | null, avatar_url: string | null, [key: string]: any }
interface GitHubEmail { email: string, primary: boolean, verified: boolean }
interface GitHubTokenResponse { access_token: string, error?: string, error_description?: string }

// Twitter/X
interface TwitterUser { id: string, username: string, name: string, email?: string, profile_image_url?: string }
interface TwitterTokenResponse { access_token: string, token_type: string, expires_in: number, scope: string, error?: string, error_description?: string }

// Google (local to google.ts)
interface GoogleUser { id: string, email: string, verified_email: boolean, name: string, given_name: string, family_name: string, picture: string, locale: string }
interface GoogleTokenResponse { access_token: string, token_type: string, expires_in: number, error?: string, error_description?: string }

// Facebook (local to facebook.ts)
interface FacebookUser { id: string, email?: string, name: string, picture?: { data: { url: string } } }
interface FacebookTokenResponse { access_token: string, token_type: string, expires_in: number, error?: { message: string, type: string, code: number } }
```

## Exceptions

```typescript
class InvalidStateException extends Error { name = 'InvalidStateException' }
class ConfigException extends Error { name = 'ConfigException' }
```

Providers throw `ConfigException` when `clientId`, `clientSecret`, or `redirectUrl` are missing.

## OAuth2 Flow

### 1. Get Auth URL
```typescript
const github = new GitHubProvider({ clientId: '', clientSecret: '', redirectUrl: '' })
const authUrl = await github.scopes(['user:email', 'read:org']).getAuthUrl()
// Each provider reads actual config from config.services.<provider>
// The constructor config is overridden by getConfig() in each provider
```

### 2. Handle Callback
```typescript
const token = await github.getAccessToken(code)
const user = await github.getUserByToken(token)
// user: { id, nickname, name, email, avatar, token, raw }
```

### 3. User from Existing Token
```typescript
const user = await github.userFromToken(existingToken)
// Calls getUserByToken internally, adds token to result
```

## Configuration Source

All providers read from the Stacks config object:
```typescript
config.services.github   // { clientId, clientSecret, redirectUrl, scopes }
config.services.google   // { clientId, clientSecret, redirectUrl, scopes }
config.services.facebook // { clientId, clientSecret, redirectUrl, scopes }
config.services.twitter  // { clientId, clientSecret, redirectUrl, scopes }
```

Each provider's `getConfig()` method reads these and also calls `this.setScopes()` with the configured scopes.

## Dependencies
- `@stacksjs/api` -- `fetcher` used for all HTTP requests (provides `.withHeaders()`, `.get()`, `.post()`)
- `@stacksjs/config` -- provides `config.services.*` for provider credentials
- `node:buffer` and `node:crypto` -- used by Twitter provider for PKCE and Basic auth

## Gotchas
- The `ProviderConfig` constructor params are largely ignored -- each provider's `getConfig()` reads from `config.services.*` instead
- `getAuthUrl()` returns `Promise<string>` (not `string`) -- it is async
- `getTokenUrl()` is `protected` -- not part of the public `ProviderInterface`
- GitHub joins scopes with space, Facebook with comma, Google with space, Twitter with space
- Facebook uses GET for token exchange; all others use POST
- Facebook `nickname` is always `null`
- Twitter requires `getAuthUrl()` before `getAccessToken()` because the code verifier is stored as instance state
- Twitter uses its own PKCE implementation via `node:crypto`, not the base class `getCodeVerifier()`/`getCodeChallenge()`
- Google auth URL includes `access_type: 'offline'` and `prompt: 'consent'` for refresh token support
- The base class state token is 40 hex chars (20 bytes), not the 20 chars stated in some docs
- `userFromToken()` spreads the user object and adds/overwrites the `token` field
- All providers include the full API response in `SocialUser.raw`
- Errors from GitHub/Google/Twitter are flat objects with `error` + `error_description`; Facebook nests it under `error.message`

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.