A small desktop app for grabbing auth tokens out of your APIs — across every environment, without hand-editing requests or pasting JWTs into a decoder.
If you test or develop against an authenticated API, the same chore keeps coming
back: fire a login request at dev, then beta, then prod; dig the token out of the
response; copy it; paste Bearer in front of it; drop it into Postman or curl.
When it expires, do it again. When a second API has a different login shape, keep
a second snippet around for that too.
Login Tool collapses that into one window. Environments are described in a config file (or through the UI), accounts are stored per environment, and one click gives you every token the response contained — decoded, with a live expiry countdown and one-click copy in the shape you actually need.
- Any auth API. Method, headers, and request body are configuration, not code.
JSON, form-urlencoded, and raw bodies are supported, with
{{variable}}substitution. - Every token at once. One login lists all returned tokens — access, refresh, id, session, whatever your API sends — each with its own expiry badge.
- Reads tokens anywhere. From a nested JSON path, an explicit name-to-path
mapping, a plain-text body, or response headers like
Set-Cookie. - Copy how you need it. Raw token,
Bearer <token>, or a fullAuthorization: Bearer <token>header. - JWTs decoded in place. Header and payload, plus a countdown that ticks down to expiry. Signatures are never verified — this is a viewer, not a validator.
- Passwords encrypted at rest via the OS keychain.
- Manual, single-shot logins. Nothing retries, nothing polls in the background.
- Follows the system light/dark theme.
Download the .dmg for your architecture from the
latest release, open it, and
drag Login Tool into Applications.
| Chip | File |
|---|---|
| Apple silicon | logintool-<version>-mac-arm64.dmg |
| Intel | logintool-<version>-mac-x64.dmg |
Or install from the terminal:
curl -fsSL https://raw.githubusercontent.com/wly-p/logintool/main/scripts/install.sh | shThat script picks the right architecture, downloads the matching build, and moves
it into /Applications. If piping a script into a shell makes you uneasy — fair —
read it first; it is about sixty lines.
Works on macOS, Windows, and Linux. Requires Node 20 or newer.
git clone https://github.com/wly-p/logintool.git
cd logintool
npm install
npm start # build and open the window
npm run dist # or build installers into release/Prebuilt Windows and Linux downloads are not published yet; build from source in the meantime.
Builds are not signed with an Apple Developer ID, so macOS will refuse to open the app on first launch when it has been quarantined. This is expected, and you should only work around it if you trust the source.
Right-click the app in Applications and choose Open, then confirm. Or go to System Settings → Privacy & Security and click Open Anyway next to the blocked-app notice. You only need to do this once.
Files fetched with curl are not quarantined, so the install script above will not
trigger that prompt. Convenient, but worth knowing: it means Gatekeeper never
checked the app.
The repo ships with a mock auth server, so you can see the whole thing working without pointing it at anything real:
mkdir -p .local-config
cp examples/mock.config.yaml .local-config/config.yaml
npm run mock # terminal 1
LOGINTOOL_CONFIG_DIR="$(pwd)/.local-config" npm start # terminal 2Add an account with any username and password, then hit Log in. The six
environments each demonstrate a different response shape. Passwords wrong,
boom, slow, garbage, and empty trigger the corresponding failure modes.
Two files live side by side, split by whether they are safe to share:
| File | Contents | Share it? |
|---|---|---|
config.yaml |
Environments and request specs | Yes — no secrets |
accounts.yaml |
Credentials, passwords encrypted | Never |
Their location, in priority order:
LOGINTOOL_CONFIG_DIR, if set- otherwise the per-platform data directory:
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/logintool/ |
| Windows | %APPDATA%\logintool\ |
| Linux | ~/.config/logintool/ |
The status bar at the bottom of the window shows the active directory; click it to reveal it in your file manager, or use Edit config.yaml to open the file directly. Reload re-reads both files without restarting.
Point LOGINTOOL_CONFIG_DIR at a folder in your team's repo to share one
config.yaml between everyone, while each person keeps their own accounts.yaml
locally.
version: 1
# Shared request shape. Every environment inherits it and can override any part.
defaults:
request:
method: POST
headers:
Content-Type: application/json
body:
type: json
template: '{"username": "{{username}}", "password": "{{password}}"}'
response:
tokens: result
environments:
# Same API in several environments: only the url differs.
- name: dev
request:
url: https://auth-dev.example.com/user/login
- name: prod
request:
url: https://auth.example.com/user/login
# A different API: override the parts that differ.
- name: keycloak
request:
url: https://sso.example.com/realms/demo/protocol/openid-connect/token
headers:
Content-Type: application/x-www-form-urlencoded
body:
type: form
fields:
grant_type: password
client_id: "{{client_id}}"
username: "{{username}}"
password: "{{password}}"
response:
tokens:
access: access_token
refresh: refresh_tokenMerge rules. Headers merge key by key, so an environment can add one header
without restating the others. body and response.tokens are replaced wholesale,
so switching to a different API shape is unambiguous. Merge semantics cannot
express removing an inherited header — only overriding or adding one.
| Field | Notes |
|---|---|
url |
Required. Supports {{variables}}. |
method |
Defaults to POST. |
headers |
Mapping. Values support {{variables}}. |
body.type |
json, form, or raw. Omit body for no body. |
body.template |
For json and raw: the body as a string. |
body.fields |
For form: a mapping, url-encoded on send. |
Variables available in any template: {{username}}, {{password}}, and every key
in the account's extra map. A missing variable is an error raised before the
request leaves the app — a literal {{password}} never reaches the network. For
json bodies, substituted values are escaped, so a password containing " or \
still produces valid JSON.
| Field | Notes |
|---|---|
format |
json (default) or text. |
tokens |
For json: a dot path, or a { name: path } mapping. For text: the name to give the whole body. |
headers |
Optional { name: headerName } — tokens read from response headers. |
With a dot path, every scalar field under that object becomes a token, so a login that starts returning an extra token shows it without a config change. An empty path reads the top level of the response.
Managed from the UI; you rarely need to touch it.
dev:
- id: 6f1c4b2a-...
name: admin
username: admin@example.com
password: "enc:v1:BASE64..."
extra:
client_id: my-clientEverything under extra becomes a {{variable}} in the request templates — that
is how OAuth flows get their client_id, scope, or tenant without any
API-specific code.
Environments and accounts can both be managed without opening a file. Use the +
at the end of the tab strip to add an environment, or Edit environment for the
current one.
Values inherited from the shared defaults block are tagged, and saving writes
back only what actually differs — the defaults block stays intact rather than
being copied into every environment.
Note that saving from the UI rewrites config.yaml with js-yaml, which does not
preserve comments. Keep reference notes in examples/ rather than in your live
config.
- Passwords are encrypted with Electron's
safeStorage, backed by the OS keychain (Keychain on macOS, DPAPI on Windows, libsecret on Linux). They are stored asenc:v1:<base64>. - If no keychain is available, the app says so in a banner and falls back to plain text rather than silently pretending to be secure.
- The encryption key is tied to the application, so an
accounts.yamlcopied from another machine cannot be decrypted. Those accounts are flagged for re-entry instead of corrupting the file. - Because macOS builds are unsigned, updating the app may re-prompt for keychain
access. Unsigned builds carry an ad-hoc signature whose hash changes every
build, and macOS ties keychain permission to that signature. Expect a
"Login Tool wants to use your confidential information" dialog after an
update; allow it and stored passwords keep working. Deny it and the affected
accounts show as
password unreadableuntil you retype the password. A proper Developer ID signature would make this go away — it is on the roadmap, not done. - Passwords never leave the main process. The UI receives a "has password" flag, never the value.
accounts.yamlis not meant to be committed. The repo's.gitignorealready excludes it.- JWTs are decoded for display only. Signatures are never verified.
npm start # build and run
npm run watch # rebuild on save (run `npx electron .` separately)
npm run typecheck
npm test # unit and integration tests
npm run mock # mock auth server on 127.0.0.1:8787
npm run screenshot # regenerate docs/img (needs the mock server running)src/
main/ main process (Node)
main.ts window and IPC
preload.ts contextBridge surface
api.ts executes one login from an environment spec
config/ config.yaml + accounts.yaml, validation, encryption
request/ template substitution, request building, token extraction
renderer/ UI (browser context)
scripts/
mock-server.mjs dependency-free mock auth server
screenshot.cjs regenerates the README screenshots
install.sh macOS installer used by the curl one-liner
examples/ ready-to-use configuration samples
The request/ and config/ modules are pure functions with no Electron imports,
which is what the test suite covers; api.test.ts runs against the mock server.
MIT — see LICENSE.


