Skip to content

Repository files navigation

Bohurupee (বহুরূপী)

CI

Local fake identity provider for social login development.

Run it on your machine and point any OAuth or OpenID Connect client at http://127.0.0.1:4190. The client thinks it is talking to Google, GitHub, Apple, or a provider you invent. Nothing leaves localhost.

Laravel Socialite is an optional adapter (milon/bohurupee-laravel). You do not need it, or Go, to use the server.

DEV ONLY. Listens on loopback unless you override that on purpose. See SECURITY.md.

Docs: bohurupee.milon.im

Install

Download a release binary, install with Homebrew, run the container, or build from source. The server is one file. It does not need Node, PHP, or a database.

Homebrew

brew install --cask milon/bohurupee/bohurupee
bohurupee init
bohurupee

That taps milon/homebrew-bohurupee. macOS and Linux are both in the cask.

Release binary

Latest release ships bohurupee, bohurupee.example.yaml, and checksums.txt. Current line: v0.2.1 (Homebrew cask tracks the tag automatically). The next cut is v0.5.0 — see CHANGELOG.md.

OS Architecture File
macOS Apple silicon bohurupee_<version>_darwin_arm64.tar.gz
macOS Intel bohurupee_<version>_darwin_amd64.tar.gz
Linux x86_64 bohurupee_<version>_linux_amd64.tar.gz
Linux arm64 bohurupee_<version>_linux_arm64.tar.gz
Windows x86_64 bohurupee_<version>_windows_amd64.zip
# example: macOS Apple silicon. Replace VERSION (e.g. 0.2.1). Check checksums.txt first.
VERSION=0.2.1
curl -fsSL -o bohurupee.tar.gz \
  "https://github.com/milon/bohurupee/releases/download/v${VERSION}/bohurupee_${VERSION}_darwin_arm64.tar.gz"
tar -xzf bohurupee.tar.gz
./bohurupee init
./bohurupee

init writes bohurupee.yaml in the current directory (Alice, Bob, Carol, and the built-in provider profiles). It will not replace a file that is already there unless you pass --force. Then start the server with no flags: it loads ./bohurupee.yaml on its own.

Open http://127.0.0.1:4190/. The home page lists personas, endpoints, and URLs you can copy.

Docker

The image is the static binary on scratch. It has no shell. Inside the container the process listens on 0.0.0.0 so Docker can publish the port. Publish that port on loopback only:

docker run --rm -p 127.0.0.1:4190:4190 ghcr.io/milon/bohurupee:v0.2.1

Mount your host config at /bohurupee.yaml (or pass --config). Personas, profiles, and the rest of the file load as usual. A bind: 127.0.0.1 in that file is upgraded to 0.0.0.0 inside Docker so port publishing still works; override with an explicit --bind if you need something else:

docker run --rm \
  -p 127.0.0.1:4190:4190 \
  -v "$PWD/bohurupee.yaml:/bohurupee.yaml:ro" \
  ghcr.io/milon/bohurupee:v0.2.1

Do not map 0.0.0.0:4190 on a shared network. To write a starter file on the host:

docker run --rm -v "$PWD:/work" -w /work ghcr.io/milon/bohurupee:v0.2.1 init

From source

Requires Go 1.25+. See CONTRIBUTING.md for the toolchain and library list.

go build -o ./bohurupee ./cmd/bohurupee
./bohurupee init
./bohurupee

Use it

Bohurupee speaks the authorization-code flow. Pick a provider slug — any URL-safe name — and treat http://127.0.0.1:4190/{provider} as the issuer.

google, github, and acme are all valid. The slug is only a namespace. Alice signed in through /google is google:alice. The same person through /acme is acme:alice.

Point a client at it

Setting Value
Issuer http://127.0.0.1:4190/google (or any other slug)
Discovery http://127.0.0.1:4190/google/.well-known/openid-configuration
Authorize GET /google/authorize
Token POST /google/token
Userinfo GET /google/userinfo
JWKS GET /google/jwks
Client ID any string, for example dev-client
Client secret any string. It is accepted and not checked
Redirect URI your app's callback. Send the same value to authorize and token
Scopes openid profile email if you want an id_token

Authorization-code clients that cannot do discovery can hard-code the three endpoints above. OIDC clients should use the discovery document. A worked example for Auth.js and other stacks is in docs/content/any-framework.md (also on the docs site). A copy-paste Auth.js provider lives in examples/authjs.

Sign in from a browser

Start the server, then open an authorize URL. The consent page lists the personas from your config. Click one. Bohurupee redirects to redirect_uri with ?code= and the state you sent.

http://127.0.0.1:4190/google/authorize?client_id=dev-client&redirect_uri=http%3A%2F%2F127.0.0.1%3A9999%2Fcallback&response_type=code&state=xyz

The home page has this URL ready to copy, filled in with the address the process is actually listening on.

Your app then POSTs the code to /google/token and calls /google/userinfo with the access token. Codes are single-use and last 2 minutes. Access tokens last 1 hour. Both live in memory and disappear when the process exits.

Skip the consent page

For curl, tests, and scripts, add ?auto=alice (or bob, carol, …). The server redirects immediately as that persona. BOHURUPEE_AUTO_APPROVE=1 does the same for every authorize request.

curl -sI 'http://127.0.0.1:4190/google/authorize?client_id=dev-client&redirect_uri=http://127.0.0.1:9999/callback&response_type=code&state=xyz&auto=alice'

The callback host does not need to be running. Read code from the Location header. Step-by-step token and userinfo calls are in examples/curl/README.md. From a git checkout:

./examples/curl/run-all.sh
python3 examples/oidc-client/client.py

The first script returns generic userinfo for /acme. The second returns GitHub-shaped userinfo for /github. The Python client discovers the issuer, uses PKCE, and verifies the id_token.

Browser tests can call POST /__login instead of adding auto= to every authorize URL. Playwright and PHP helpers live in examples/playwright and examples/php. The consent page has a Deny button that sends the app error=access_denied.

What you get back

With no provider profile, userinfo is the same shape for every slug:

{
  "id": "google:alice",
  "sub": "google:alice",
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice Admin",
  "nickname": "alice",
  "avatar": "https://api.dicebear.com/9.x/identicon/svg?seed=alice"
}

id and sub are {provider}:{persona}. Request scope containing openid and the token response also includes an RS256 id_token with given_name / family_name (from the persona name) and at_hash for the access token. Verify it against /{provider}/jwks (alias /{provider}/auth/keys). The signing key is stored under your user config directory (…/bohurupee/oidc.key) so it survives restarts.

Personas and config

From the project directory:

bohurupee init

That writes bohurupee.yaml. Edit it, then start the server. It reads the file on startup as a sparse overlay on the built-in defaults — omit any key you do not want to change, except personas, which is always required. You do not rebuild to change people. While the server is running, POST /__reload (loopback only) applies YAML changes without a restart:

curl -s -X POST http://127.0.0.1:4190/__reload

--config writes or loads a different path. --force replaces a config that is already there.

If you would rather not run init, copy bohurupee.example.yaml to bohurupee.yaml. The two files start out the same.

port: 4190
bind: 127.0.0.1
pkce: optional          # optional | required | forbidden
idToken: openid         # openid (only when scope has openid) | always
# refreshTokens: true   # opt-in; off by default

personas:
  - id: alice
    email: alice@example.com
    name: Alice Admin
    nickname: alice
    claims:
      role: admin
  - id: bob
    email: bob@example.com
    name: Bob User
    email_verified: false
    claims:
      role: user

providerProfiles:
  github:
    responseTemplate: github

--bind and --port on the command line override the file. If ./bohurupee.yaml exists and you omit --config, that file is loaded.

PKCE is optional by default. Set pkce: required when you want every authorize request to send a code_challenge. The token request must then include the matching code_verifier.

Refresh tokens are off by default. With refreshTokens: true, code exchange includes refresh_token, discovery lists refresh_token as a supported grant, and grant_type=refresh_token issues a new access token. Default token JSON stays unchanged when the flag is omitted or false.

Authorize accepts OIDC prompt=login (always show consent; ignores the last-persona and loginAs cookies) and login_hint=<persona> (pre-selects that persona on the consent page). ?auto= still skips consent for tests.

Optional clients entries pin redirect_uris per client_id. A configured client rejects surprise callbacks. Unlisted clients still work when openClient: true (the default). Set openClient: false to require every client_id to appear under clients.

Provider-shaped userinfo

A slug that matches a built-in template uses that shape. /github/userinfo adds login, avatar_url, html_url, and type with no config. Any other slug, such as /acme or a Socialite driver that has no template, uses the default template: the generic fields plus login, username, preferred_username, display_name, picture, avatar_url, and profile_image_url.

Set responseTemplate: generic on a profile to return only the generic fields. The example config also sets protocol and endpoints for a few slugs (facebook /me, Apple form_post).

Generic fields (id, email, name, nickname, avatar) stay present so client getters keep working, unless a persona response: overlay sets the same key (for example an empty avatar).

Apple's profile defaults to response_mode=form_post and always returns an id_token. Facebook's profile also serves userinfo at /facebook/me.

Custom keys without a built-in template:

providerProfiles:
  staffdir:
    response:
      title: Engineer
      username: "{{nickname}}"

Merge order, templates, and placeholders are in docs/content/provider-profiles.md.

Laravel Socialite

Optional. Install milon/bohurupee-laravel, set BOHURUPEE_ENABLED=true and BOHURUPEE_URL=http://127.0.0.1:4190, and keep using Socialite::driver('google'). The driver name is the provider slug. Setup, the production guard, and a minimal app are in docs/content/socialite.md and examples/laravel-socialite.

Commands

bohurupee init
bohurupee init --config ./other.yaml
bohurupee init --force

init writes a starter bohurupee.yaml in the current directory. Pass --config to choose another path. --force replaces a file that already exists.

Flags

Flag Default Purpose
--bind 127.0.0.1 Listen host
--port 4190 Listen port
--config ./bohurupee.yaml if present Personas, PKCE, profiles, bind, port
--version Print the version and exit
--dangerously-bind-all-interfaces off Allow a non-loopback bind

0.0.0.0, LAN addresses, and other non-loopback hosts are refused unless that last flag is set. The Docker image sets BOHURUPEE_IN_DOCKER=1, which is the same override inside the container.

Endpoints

Method Path Notes
GET /{provider}/authorize client_id, redirect_uri, response_type=code, state. Consent, ?auto=<persona>, or ?deny=1. prompt=login forces consent; login_hint pre-selects a persona. Optional per-client redirect_uris. Failed requests with a valid redirect_uri return ?error= (RFC 6749). form_post auto-posts code or error.
POST /{provider}/token Form body and/or HTTP Basic. grant_type=authorization_code (and refresh_token when enabled). Errors are {"error":"invalid_grant"} (and friends). Send code_verifier when a PKCE challenge was used. CORS for loopback origins.
GET /{provider}/userinfo Authorization: Bearer …. CORS for loopback origins.
GET /{provider}/.well-known/openid-configuration Issuer, authorize, token, userinfo, jwks. CORS for loopback origins.
GET /{provider}/jwks JWKS. Alias: /{provider}/auth/keys
GET/POST /__login Test helper. persona (and optional provider). Sets a cookie so the next authorize skips consent.
POST /__reload Loopback only. Reload YAML (personas, PKCE, profiles, refreshTokens) without restart.

Security

This is a fake identity provider for development on your machine.

  • Non-loopback binds are refused unless you pass --dangerously-bind-all-interfaces.
  • In Docker, keep the published host port on 127.0.0.1.
  • Any client_id and client_secret are accepted.
  • Codes and tokens are process memory. They are gone when the process exits.
  • Do not point a production app at it, and do not expose it on a LAN or the public internet.

License

MIT

About

Local fake identity provider for OAuth and OpenID Connect development. Point any client at localhost and sign in as a persona. Nothing leaves your machine.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages