Authentication engine for Pangea Alliance Rails applications. UUID-based auth with CAS server/client, SuperTokens SSO, invitation-only registration, and Rails generators.
Add to your Gemfile:
# From GitHub Packages (when published):
# gem "pangea-auth", source: "https://rubygems.pkg.github.com/cocoonventures"
# From local path (monorepo):
gem "pangea-auth", path: "gems/pangea-auth"Then:
bundle installrails generate pangea:auth:install
rails db:migrateThis creates:
- Migrations: users, sessions, invitations (all UUID PKs)
- Models: User (with
Authenticatableconcern), Session, Invitation (withInvitableconcern), Current - Controllers: SessionsController, PasswordsController, InvitationsController
- Views: Sign-in, password reset, invitation acceptance forms
- Initializer:
config/initializers/pangea_auth.rbwith configuration DSL - Routes: session, password, invitation endpoints
If this app should act as a CAS identity provider:
rails generate pangea:auth:cas
rails db:migrateThis adds:
- CasServiceTicket model: short-lived SSO tickets (5-min expiry, single-use)
- CasRegisteredService model: service whitelist with glob URL patterns
- CasController: CAS 1.0/2.0/3.0 endpoints (login, validate, serviceValidate, logout)
- Shared sign-in form: DRY partial used by both local login and CAS login
- Routes:
/cas/login,/cas/validate,/cas/serviceValidate,/cas/p3/serviceValidate,/cas/logout
For OAuth (Google, LinkedIn, Apple), passwordless (magic link), and MFA:
rails generate pangea:auth:supertokensThis adds:
- SuperTokensClient: REST API wrapper for the SuperTokens core service
- Inflection: ensures
SuperTokensClientclass name works with Zeitwerk
Then add credentials:
bin/rails credentials:editsupertokens:
connection_uri: https://your-supertokens-instance.com
api_key: your-api-key# config/initializers/pangea_auth.rb
Pangea::Auth.configure do |config|
# Core
config.uuid_primary_keys = true # UUID PKs on all auth tables
config.session_expiry = 30.days # Session lifetime
config.invitation_expiry = 14.days # Invitation token lifetime
config.invitation_required = true # No public signup
# CAS (uncomment after running cas generator)
# config.cas_enabled = true
# config.cas_mode = :server # :server or :client
# SuperTokens (uncomment after running supertokens generator)
# config.supertokens_enabled = true
# config.supertokens_connection_uri = Rails.application.credentials.dig(:supertokens, :connection_uri)
# config.supertokens_api_key = Rails.application.credentials.dig(:supertokens, :api_key)
# config.supertokens_providers = [:google, :linkedin, :apple]
# Callbacks
# config.after_sign_in = ->(user, session) { }
# config.after_invitation_accepted = ->(user, invitation) { }
endThe generated User model includes the Pangea::Auth::Authenticatable concern:
class User < ApplicationRecord
include Pangea::Auth::Authenticatable
# Provides: has_secure_password, role enum (observer→super_admin),
# email validation/normalization, default role callback
endRole tiers: observer (0), analyst (1), member (2), admin (3), super_admin (4)
The generated Invitation model includes the Pangea::Auth::Invitable concern:
class Invitation < ApplicationRecord
include Pangea::Auth::Invitable
# Provides: token generation, expiry, status enum, accept! method
endWhen running as a CAS server, the app provides:
| Endpoint | Protocol | Description |
|---|---|---|
GET /cas/login |
1.0 | Login form or SSO redirect |
POST /cas/login |
1.0 | Authenticate + issue ticket |
GET /cas/validate |
1.0 | Text validation (yes/no) |
GET /cas/serviceValidate |
2.0 | XML validation + attributes |
GET /cas/p3/serviceValidate |
3.0 | XML validation + attributes |
GET /cas/logout |
1.0 | Destroy session + redirect |
Protocol parameters:
renew=true— force re-authentication (bypass SSO)gateway=true— silent SSO check (redirect without prompting)format=JSON— JSON response on serviceValidate
Service registry:
- When
CasRegisteredServicerecords exist, only registered services can authenticate - When empty, all services are allowed (open mode for development)
- Uses glob URL patterns:
https://myapp.com/**
Register a service:
CasRegisteredService.create!(
name: "My Client App",
service_url_pattern: "https://myapp.example.com/**",
enabled: true
)For apps that delegate auth to an external CAS server (e.g., ally.vc):
# Gemfile
gem "rack-cas"
# config/initializers/rack_cas.rb
Rails.application.config.rack_cas.server_url = "https://ally.vc/cas"The CAS server returns user attributes in the serviceValidate response. Use these to find-or-create local user records:
# In your CAS callback controller:
user = User.find_or_create_by!(email: cas_user.email) do |u|
u.first_name = cas_user.extra_attributes["firstName"]
u.last_name = cas_user.extra_attributes["lastName"]
u.role = cas_user.extra_attributes["role"]
u.verified = true
endThe gem uses Minitest. When installed in an app:
# Run auth-related tests
bin/rails test test/models/user_test.rb
bin/rails test test/controllers/sessions_controller_test.rb
# Run generator tests
bin/rails test test/generators/- Ruby >= 3.2
- Rails >= 8.0
- PostgreSQL (for UUID primary keys)
- bcrypt (for has_secure_password)
MIT License. Copyright Pangea Alliance.