comrade v0.2.0
Comrade
A Crystal OAuth library inspired by Laravel Socialite for simple, elegant OAuth authentication with enterprise SSO support.
Table of Contents
Background
Comrade provides a simple, elegant interface for OAuth authentication with various providers like GitHub, Google, and more. Inspired by Laravel Socialite, it offers a fluent API for handling OAuth flows in Crystal applications with enterprise-ready SSO capabilities through WorkOS.
The library follows a singleton pattern with a centralized Manager for provider configuration and supports both confidential and public client flows, including PKCE for enhanced security. It seamlessly integrates with popular OAuth providers while also offering enterprise SSO through SAML and OIDC connections.
Install
Add the dependency to your shard.yml:
dependencies:
comrade:
github: ButterbaseApp/comrade
version: ~> 0.1.0
Run shards install to install the dependency.
Usage
Basic Setup
require "comrade"
# Configure a provider from environment variables
Comrade.register_provider(
:github,
"GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET",
"http://localhost:3000/auth/github/callback",
["user:email"]
)
# Configure WorkOS for enterprise SSO
Comrade.register_provider(
:workos,
"WORKOS_CLIENT_ID",
"WORKOS_CLIENT_SECRET",
"http://localhost:3000/auth/workos/callback"
)
# Or configure globally
Comrade.configure do |config|
config.http_timeout = 30
end
Provider Configuration
Environment Variables
# Configure GitHub OAuth
Comrade.register_provider(
:github,
"GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET",
"http://localhost:3000/auth/github/callback",
["user:email"]
)
# Configure Google OAuth
Comrade.register_provider(
:google,
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
"http://localhost:3000/auth/google/callback",
["openid", "profile", "email"]
)
# Configure WorkOS enterprise SSO
Comrade.register_provider(
:workos,
"WORKOS_CLIENT_ID",
"WORKOS_CLIENT_SECRET",
"http://localhost:3000/auth/workos/callback"
)
# Configure Keycloak with realm
Comrade.register_provider(
:keycloak,
"KEYCLOAK_CLIENT_ID",
"KEYCLOAK_CLIENT_SECRET",
"http://localhost:3000/auth/keycloak/callback"
)
Hash Configuration
config = {
"name" => "github",
"client_id" => "your-github-client-id",
"client_secret" => "your-github-client-secret",
"redirect_uri" => "http://localhost:3000/auth/github/callback",
"scopes" => ["user:email"]
}
Comrade.register_provider(:github, config)
OAuth Flow
1. Redirect User for Authorization
# Get provider instance
github = Comrade.driver(:github)
# Generate authorization URL with state parameter
auth_url = github.redirect(
scopes: ["user:email"],
state: "random-state-string"
)
# Redirect user to auth_url
redirect_to auth_url
2. Handle Callback and Get User
# After user authorizes, exchange code for token
code = params["code"]
state = params["state"]
# Get access token
token = github.get_token(code, state: state)
# Get user information
user = github.user(token)
puts "User: #{user.name} (#{user.email})"
puts "Avatar: #{user.avatar}"
3. Token Management
# Check if token is expired
if token.expired?
# Refresh token if available
if token.refreshable?
token = github.refresh_token(token.refresh_token.not_nil!)
end
end
# Revoke token (if supported by provider)
github.revoke_token(token.access_token)
Supported Providers
Provider Implementation Status
- GitHub - OAuth 2.0 with email fetching, basic scopes
- Google - OAuth 2.0 + OpenID Connect, PKCE support, token revocation
- Facebook/Meta - OAuth 2.0, Graph API, permissions system
- Twitter/X - OAuth 2.0, user profile and tweets access
- Discord - OAuth 2.0, rich user/guild data
- WorkOS - Enterprise SSO via SAML/OIDC, organization-based authentication
- Authentik - OAuth 2.0 + OpenID Connect, self-hosted identity provider, token revocation
- Keycloak - OAuth 2.0 + OpenID Connect, enterprise identity management, realm-based architecture
- Microsoft - OAuth 2.0 + OpenID Connect, Azure AD support
- Slack - OAuth 2.0, workspace integration
- LinkedIn - OAuth 2.0, professional profile data
- Apple - OAuth 2.0 + OpenID Connect, Sign in with Apple
- GitLab - OAuth 2.0, developer platform integration
- Bitbucket - OAuth 2.0, Atlassian ecosystem
- Spotify - OAuth 2.0, music and user data
- Strava - OAuth 2.0, fitness and activity data
GitHub
github = Comrade.driver(:github)
auth_url = github.redirect(scopes: ["user", "repo"])
user = github.user(token)
# GitHub automatically fetches primary email if not included
# Default scopes: ["user:email"]
google = Comrade.driver(:google)
auth_url = google.redirect(
scopes: ["openid", "profile", "email"],
# Google supports offline access for refresh tokens
code_verifier: google.generate_code_verifier # PKCE for public clients
)
user = google.user(token)
# Default scopes: ["openid", "profile", "email"]
# Supports token revocation
Facebook/Meta
facebook = Comrade.driver(:facebook)
auth_url = facebook.redirect(
scopes: ["email", "public_profile"],
state: "random-state-string"
)
user = facebook.user(token)
# Facebook uses Graph API for user data
# Default scopes: ["email", "public_profile"]
# Supports token revocation via permission removal
# Note: Requires Facebook App with OAuth configuration
Twitter/X
twitter = Comrade.driver(:twitter)
auth_url = twitter.redirect(
scopes: ["tweet.read", "users.read"],
state: "random-state-string"
)
user = twitter.user(token)
# Twitter/X uses OAuth 2.0 with v2 API
# Default scopes: ["tweet.read", "users.read"]
# Supports token revocation
# Note: Requires Twitter Developer App with OAuth 2.0 enabled
Discord
discord = Comrade.driver(:discord)
auth_url = discord.redirect(
scopes: ["identify", "email", "guilds"],
state: "random-state-string"
)
user = discord.user(token)
# Get user's guilds (servers)
guilds = discord.get_user_guilds(token)
# Discord uses OAuth 2.0 with rich user/guild data
# Default scopes: ["identify", "email"]
# Supports avatar URLs, guild information, and token revocation
# Note: Requires Discord Application with OAuth2 configured
WorkOS
WorkOS provides enterprise-ready SSO capabilities through SAML and OIDC connections, perfect for B2B applications.
# Configure WorkOS provider
Comrade.register_provider(
:workos,
"WORKOS_CLIENT_ID",
"WORKOS_CLIENT_SECRET",
"http://localhost:3000/auth/workos/callback"
)
# Connection-based SSO (recommended for specific organizations)
workos = Comrade.driver(:workos)
auth_url = workos.redirect(
connection: "conn_123456789", # Specific SSO connection
domain_hint: "acme.com", # Optional: help users select IdP
login_hint: "user@acme.com", # Optional: pre-fill email
state: "random-state-string"
)
# Organization-based SSO (for enterprise customers)
auth_url = workos.redirect(
organization: "org_123456789", # WorkOS organization
state: "random-state-string"
)
# Direct OAuth providers (via WorkOS)
auth_url = workos.redirect(
provider: "GoogleOAuth", # "GoogleOAuth", "MicrosoftOAuth", "GitHubOAuth", "AppleOAuth"
state: "random-state-string"
)
# Exchange authorization code for user
code = params["code"]
token = workos.get_token(code, state: state)
user = workos.user(token)
# Access enterprise-specific data
user.raw["organization_id"]?.try(&.as_s) # Organization ID
user.raw["connection_id"]?.try(&.as_s) # Connection ID
user.raw["connection_type"]?.try(&.as_s) # "SAML", "OIDC", etc.
user.raw["groups"]?.try(&.as_a) # User groups from IdP
# WorkOS enterprise features
# - No refresh tokens (tokens are long-lived)
# - Supports SAML and OIDC providers
# - Organization and connection management
# - Group-based access control
# - Token revocation support
# Note: Requires WorkOS account with SSO connections configured
Authentik
Authentik is an open-source identity provider that supports OAuth 2.0 and OpenID Connect. Perfect for self-hosted authentication solutions.
# Configure Authentik provider with base URL
Comrade.register_provider(
:authentik,
{
"name" => "authentik",
"client_id" => ENV["AUTHENTIK_CLIENT_ID"],
"client_secret" => ENV["AUTHENTIK_CLIENT_SECRET"],
"redirect_uri" => "http://localhost:3000/auth/authentik/callback",
"scopes" => ["openid", "profile", "email"],
"base_url" => ENV["AUTHENTIK_BASE_URL"] # e.g., "https://authentik.company.com"
}
)
# Or configure with domain-based redirect URI (base URL extracted automatically)
Comrade.register_provider(
:authentik,
"AUTHENTIK_CLIENT_ID",
"AUTHENTIK_CLIENT_SECRET",
"https://authentik.company.com/application/o/callback/"
)
# Get Authentik driver and start OAuth flow
authentik = Comrade.driver(:authentik)
auth_url = authentik.redirect(
scopes: ["openid", "profile", "email"],
state: "random-state-string"
)
# Exchange authorization code for user
code = params["code"]
token = authentik.get_token(code, state: state)
user = authentik.get_user(token)
# OpenID Connect user data
user.id # Subject identifier (sub)
user.email # User email
user.name # User display name
user.nickname # Preferred username
user.avatar # Profile picture URL
# Authentik-specific features
authentik.default_scopes # ["openid", "profile", "email"]
authentik.revoke_token(token.access_token) # Token revocation support
# PKCE support for public clients (mobile apps, SPAs)
code_verifier = authentik.generate_code_verifier
auth_url = authentik.redirect(
scopes: ["openid", "email"],
code_verifier: code_verifier,
state: "random-state-string"
)
token = authentik.get_token(code, state: state, code_verifier: code_verifier)
# Note: Requires Authentik instance with OAuth2 provider configured
Keycloak
Keycloak is an open-source identity and access management solution focused on enterprise use cases. It provides comprehensive OAuth 2.0 and OpenID Connect support with realm-based architecture for multi-tenant scenarios.
# Configure Keycloak provider with base URL and realm
Comrade.register_provider(
:keycloak,
{
"name" => "keycloak",
"client_id" => ENV["KEYCLOAK_CLIENT_ID"],
"client_secret" => ENV["KEYCLOAK_CLIENT_SECRET"],
"redirect_uri" => "http://localhost:3000/auth/keycloak/callback",
"scopes" => ["openid", "profile", "email"],
"base_url" => ENV["KEYCLOAK_BASE_URL"], # e.g., "https://keycloak.company.com"
"realm" => ENV["KEYCLOAK_REALM"] # e.g., "myrealm" or "master"
}
)
# Or configure with realm-based redirect URI (extracted automatically)
Comrade.register_provider(
:keycloak,
"KEYCLOAK_CLIENT_ID",
"KEYCLOAK_CLIENT_SECRET",
"https://keycloak.company.com/realms/myrealm/callback"
)
# Get Keycloak driver and start OAuth flow
keycloak = Comrade.driver(:keycloak)
auth_url = keycloak.redirect(
scopes: ["openid", "profile", "email"],
state: "random-state-string"
)
# Exchange authorization code for user
code = params["code"]
token = keycloak.get_token(code, state: state)
user = keycloak.get_user(token)
# OpenID Connect user data
user.id # Subject identifier (sub)
user.email # User email
user.name # User display name
user.nickname # Preferred username
user.avatar # Profile picture URL
# Keycloak-specific features
keycloak.default_scopes # ["openid", "profile", "email"]
keycloak.revoke_token(token.access_token) # Token revocation support
# End session (logout user)
logout_url = keycloak.end_session(
id_token_hint: token.id_token, # Optional: ID token from token response
post_logout_redirect_uri: "http://localhost:3000/logout"
)
# Get OpenID Connect configuration
oidc_config = keycloak.oidc_configuration
jwks = keycloak.jwks # JSON Web Key Set for token validation
# Token refresh with optional scope
refreshed_token = keycloak.refresh_token(
token.refresh_token.not_nil!,
scope: "openid profile email" # Optional: request different scopes
)
# PKCE support for public clients (mobile apps, SPAs)
code_verifier = keycloak.generate_code_verifier
auth_url = keycloak.redirect(
scopes: ["openid", "email"],
code_verifier: code_verifier,
state: "random-state-string"
)
token = keycloak.get_token(code, state: state, code_verifier: code_verifier)
# Note: Requires Keycloak server with OpenID Connect client configured
# Typical Keycloak endpoints:
# - Authorization: https://keycloak.company.com/realms/{realm}/protocol/openid-connect/auth
# - Token: https://keycloak.company.com/realms/{realm}/protocol/openid-connect/token
# - User Info: https://keycloak.company.com/realms/{realm}/protocol/openid-connect/userinfo
# - Logout: https://keycloak.company.com/realms/{realm}/protocol/openid-connect/logout
# - JWKS: https://keycloak.company.com/realms/{realm}/protocol/openid-connect/certs
API
Comrade Module
Comrade.driver(name : Symbol)- Get configured provider instanceComrade.configure(&block)- Configure global settingsComrade.register_provider(name, ...)- Register provider configurationComrade.provider_configured?(name)- Check if provider is configuredComrade.remove_provider(name)- Remove provider configuration
Provider Methods
All providers implement the following interface:
redirect(scopes, state, **options)- Generate authorization URLget_token(code, state, **options)- Exchange code for access tokenget_user(token)- Get user information using access tokenrefresh_token(refresh_token, **options)- Refresh access tokenrevoke_token(token)- Revoke access token (if supported)
User Object
user.id # String - User ID from provider
user.nickname # String? - Username/handle
user.name # String? - Full display name
user.email # String? - Email address
user.avatar # String? - Avatar URL
user.raw # JSON::Any - Raw provider response
# Helper methods
user.has_email? # Bool
user.has_name? # Bool
user.has_avatar? # Bool
user.display_name # String - name || nickname || id
user.to_h # Hash - User data as hash
user.get_raw_field("field") # Get raw field from provider response
Token Object
token.access_token # String - OAuth access token
token.refresh_token # String? - Refresh token (if available)
token.expires_in # Int32? - Token lifetime in seconds
token.scope # String? - Granted scopes
token.token_type # String? - Token type (usually "Bearer")
token.created_at # Time - Token creation time
# Helper methods
token.expired? # Bool
token.expiring_soon? # Bool (within 5 minutes by default)
token.expires_at # Time?
token.refreshable? # Bool
token.to_h # Hash - Token data as hash
Security
- State Parameter: Always use state parameters to prevent CSRF attacks
- PKCE Support: Use PKCE for public clients (mobile apps, SPAs)
- Token Storage: Store tokens securely, consider encryption
- HTTPS: Always use HTTPS in production for OAuth redirects
- Scope Limitation: Request only necessary scopes
- Token Expiration: Check token expiration before use
# Secure example with state and PKCE
provider = Comrade.driver(:google)
state = provider.generate_state
code_verifier = provider.generate_code_verifier
# Store state and code_verifier in session
session["oauth_state"] = state
session["code_verifier"] = code_verifier
auth_url = provider.redirect(
scopes: ["openid", "profile", "email"],
state: state,
code_verifier: code_verifier
)
# In callback, verify state first
if params["state"] != session["oauth_state"]
raise "Invalid state parameter"
end
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Development
- Clone the repository
- Create your feature branch (
git checkout -b my-new-feature) - Install dependencies (
shards install) - Run tests (
crystal spec) - Run linter (
ameba) - Commit your changes using Conventional Commits
- Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Commit Messages
This project follows the Conventional Commits specification. Commit messages should be structured as follows:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Common types:
feat: A new featurefix: A bug fixdocs: Documentation only changesstyle: Code formatting changes (white-space, etc)refactor: Code change that neither fixes a bug nor adds a featuretest: Adding missing tests or correcting existing testsbuild: Changes that affect the build system or external dependenciesci: Changes to CI configuration files and scriptschore: Other changes that don't modify src or test files
Examples:
feat: add GitHub OAuth providerfix(auth): resolve token expiration issuedocs: update installation instructionsci: add multi-version Crystal testing
Running Tests
# Run all tests
crystal spec
# Run specific test file
crystal spec spec/comrade_spec.cr
# Run with coverage
crystal spec --coverage
Code Style
This project uses Ameba for static analysis. Run ameba to check code style before submitting pull requests.
License
This project is licensed under the MIT License - see the LICENSE file for details.
comrade
- 2
- 0
- 0
- 0
- 2
- 5 days ago
- October 14, 2025
MIT License
Wed, 14 Jan 2026 13:17:19 GMT