Identizer

CI

A local identity provider for developing and testing auth/SSO integrations.

The problem it solves: to test "Sign in with SSO", you normally need a real Okta/Auth0/Azure/Cognito tenant, real metadata, real certificates. That's slow to set up and impossible to script in CI. Identizer is a fake-but-real IdP you run locally: point your app at it, sign in as a test user, done. No accounts, no cloud.

Quick start (60 seconds)

gem install identizer
identizer                       # boots on https://localhost:9999

It prints exactly where to point your app. Open the dashboard at https://localhost:9999/ — a demo user (demo@example.com, password password) is already there, so login works immediately. Then in your app's SSO settings:

  • OIDC / OpenID Connect → issuer https://localhost:9999 (the client reads everything else from /.well-known/openid-configuration).
  • SAML → metadata https://localhost:9999/metadata.
  • OAuth2 / Auth0 → domain localhost:9999.

Trigger login in your app, sign in as the demo user, and you're testing the real flow. (For browser/server TLS trust, see TLS — one SSL_CERT_FILE line.)

Which protocol do I need?

If you're not sure how your app talks to its IdP, match the setting it asks for:

Your app's SSO config mentions… Use Point it at
"Issuer URL", "discovery", "client ID/secret", openid OIDC https://localhost:9999
"Metadata URL/XML", "ACS", "SAML", "certificate" SAML https://localhost:9999/metadata
"Auth0 domain", /authorize + /userinfo OAuth2/Auth0 localhost:9999
"Cognito", "user pool", COGNITO_ENDPOINT Cognito COGNITO_ENDPOINT=https://localhost:9999
"LDAP bind", ldap:// LDAP identizer --ldap-port 1389

How it works (two halves)

  • a directory of sign-in identities (the pluggable "users" store), and
  • a provider that accepts auth requests, signs the user in, and hands the profile back over whichever protocol your app expects (OIDC, OAuth2, SAML, …).

Why not an existing tool?

Generic OIDC mocks (e.g. mock-oauth2-server) are JVM/Docker, and SAML-only Ruby gems (e.g. saml_idp) cover one protocol. Identizer is a single, zero-infra Ruby gem that covers OIDC + OAuth2 + a Cognito/Auth0 broker with a pluggable user directory — installable, mountable, and scriptable.

Install

# Gemfile (development/test)
gem "identizer", group: %i[development test]
bundle install

Run standalone

bundle exec identizer --port 9999
# open https://localhost:9999/  (dashboard: identities + provider cheatsheet)

Common flags: --port, --host, --url-host, --config-dir, --tls-cert, --tls-key, --password, --rs256. Anything not passed falls back to env vars (IDENTIZER_PORT, IDENTIZER_TLS_CERT/KEY, IDENTIZER_CONFIG_DIR, …).

Mount inside a Rack/Rails app

Identizer::App is a plain Rack app, so it works mounted at any path — internal links honour SCRIPT_NAME.

# config.ru
require "identizer"
run Identizer.app
# Rails config/routes.rb (development only)
mount Identizer::App.new => "/idp" if Rails.env.development?
# RSpec / rack-test
require "rack/test"

app = Identizer::App.new(
  Identizer::Configuration.new.tap do |c|
    c.config_dir = Dir.mktmpdir
    c.seed_identities = [{ email: "alice@example.com", claims: { given_name: "Alice" } }]
  end
)

Configure

Identizer.configure do |config|
  config.port = 9999
  config.shared_password = "password"          # type this to succeed; anything else exercises the error path
  config.signing = :rs256                       # :hs256 (default) or :rs256 + JWKS for clients that verify
  config.seed_identities = [
    { email: "alice@example.com", claims: { given_name: "Alice", family_name: "Doe" } }
  ]
  # config.identity_store = MyDbBackedStore.new  # plug in any object exposing #emails / #identity_for(email)
end

Other options (all have sane defaults): code_ttl / access_token_ttl / refresh_token_ttl (grant lifetimes), clients (registry that enables redirect_uri allowlisting), saml_sign_response, saml_encrypt_assertion + saml_sp_certificate, saml_allowed_acs, saml_attribute_names, ldap_port / ldaps_port, and request_logging. See lib/identizer/configuration.rb.

Identity store interface

Any object responding to this duck-typed interface can be a directory:

#emails              -> Array<String>   addresses the login form accepts
#identity_for(email) -> Identity | nil  resolve an address to an Identity

For full management through the web admin, a store also exposes the directory interface #entries, #upsert(attrs) and #delete(email) (the bundled ConfigStore and SqliteStore do).

The default Identizer::IdentityStore::ConfigStore persists identities to a JSON file the dashboard writes, seeded from config.seed_identities.

SQLite backend (optional)

Prefer a database? Add gem "sqlite3" to your Gemfile and use the bundled adapter — it implements the same directory interface, so the web admin and LDAP work against it unchanged:

bundle exec identizer --sqlite ./dev.sqlite3
require "identizer/identity_store/sqlite_store"
config.identity_store = Identizer::IdentityStore::SqliteStore.new(path: "dev.sqlite3")

sqlite3 is not a default dependency — JSON files remain the zero-infra default.

Endpoints

Most clients only need the issuer/metadata URL; the rest is discovered. Okta-style /oauth2/v1/* aliases exist for the OIDC routes (authorize/token/userinfo/keys).

Purpose Route
Dashboard / config GET /
Health GET /healthz
Login form GET /login, /authorize, /v1/authorize
Cognito hosted-UI token POST /oauth2/token
Auth0 token + profile POST /oauth/token, GET /userinfo
OIDC token / logout POST /v1/token, GET /v1/logout
OIDC introspection / revocation POST /introspect, POST /revoke
OIDC discovery / JWKS GET /.well-known/openid-configuration, /.well-known/jwks.json
SAML metadata / SSO GET /metadata, `GET
Cognito management API POST / with x-amz-target (point COGNITO_ENDPOINT here)
Auth0 Management API POST/DELETE /api/v2/clients, /api/v2/connections

LDAP listener (optional)

Apps that authenticate via LDAP can bind and search against the same directory. It's off unless you ask for it:

bundle exec identizer --port 9999 --ldap-port 1389
# ldapsearch -x -H ldap://localhost:1389 -b dc=identizer,dc=local "(mail=alice@example.com)"

Simple bind (user DN + shared password, or anonymous) and subtree search with equality / presence / substring / & | ! filters. Entries project to uid, cn, sn, givenName, mail, ou, memberOf, objectClass. Plain TCP + simple bind — a development listener, not LDAPS.

TLS

Login URLs must be https (browser popup guards reject http). Identizer uses a provided cert (--tls-cert/--tls-key, ideally mkcert-generated and locally trusted) or falls back to a self-signed cert written under config_dir. For the app's server-to-server calls, trust it via export SSL_CERT_FILE=…/cert.pem.

SAML 2.0

A real SAML IdP: it issues signed assertions (XML-DSig, RSA-SHA256) verifiable by standard SPs. Metadata at /metadata, SSO at /saml/sso (Redirect & POST bindings), SP- and IdP-initiated. Signing uses nokogiri, loaded only when a Response is produced. A development IdP — convenient, not hardened.

Development

bin/setup            # install dependencies
bundle exec rake     # rspec + rubocop
bin/console          # an IRB session with identizer loaded

License

MIT.