Skip to content

Commit e6ca682

Browse files
committed
feat(cli): improve browser login instructions and error handling for Keycloak authentication
1 parent 3d99359 commit e6ca682

2 files changed

Lines changed: 72 additions & 6 deletions

File tree

README.md

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Currently using [OpenBao](https://openbao.org) and [OpenTofu](https://opentofu.o
1515
| [OpenTofu](https://opentofu.org/docs/intro/install/) | 1.8.0 | `tofu` must be on `$PATH` |
1616
| [Podman](https://podman.io/docs/installation) | 4.0 | `podman` must be on `$PATH` |
1717
| [podman-compose](https://github.com/containers/podman-compose) | 1.0 | `podman compose` plugin or `podman-compose` |
18+
| [Python](https://python.org) | 3.12+ | `python` must be on `$PATH` |
1819
| Linux kernel || `IPC_LOCK` for mlock; set `disable_mlock = true` if unavailable (some WSL2 setups) |
1920

2021
The Vault/OpenBao CLI is **not** required on the host — `tofu output` prints ready-to-run `init` and `unseal` commands. If you do have the CLI installed, set `VAULT_CACERT` to the generated CA path.
@@ -560,7 +561,7 @@ Navigate to `https://127.0.0.1:8444/admin` and log in with your admin credential
560561

561562
1. With the `vault` client open, click the **Client scopes** tab
562563
2. Click the link named **`vault-dedicated`** (the first row, type will show as *Dedicated*)
563-
3. Click **Add mapper****By configuration**
564+
3. Click **Add mapper****Configure a New Mapper**
564565
4. Click **Group Membership**
565566
5. Set **Name** to `groups`
566567
6. Set **Token Claim Name** to `groups`
@@ -599,7 +600,7 @@ Navigate to `https://127.0.0.1:8444/admin` and log in with your admin credential
599600

600601
1. Click the **Client scopes** tab
601602
2. Click **`agent-cli-dedicated`**
602-
3. Click **Add mapper****By configuration**
603+
3. Click **Add mapper****Configure a new Mapper**
603604
4. Click **Group Membership**
604605
5. Set **Name** to `groups`
605606
6. Set **Token Claim Name** to `groups`
@@ -610,7 +611,55 @@ Navigate to `https://127.0.0.1:8444/admin` and log in with your admin credential
610611

611612
---
612613

613-
Once this is done you can verify the mapper is working before touching OpenBao — log in as your operator user and decode the resulting access token at `https://jwt.io` to confirm the `groups` claim contains `vault-operators`.
614+
Once this is done, verify the mapper before touching OpenBao (using the correct client token):
615+
616+
1. In Keycloak admin, open **Clients** -> **vault** -> **Client scopes** -> **Evaluate**
617+
2. Select user **operator** and click **Generated access token**
618+
3. Keycloak shows the decoded JSON payload directly — confirm it includes `"groups": ["vault-operators"]`
619+
4. Repeat for **Clients** -> **agent-cli** -> **Client scopes** -> **Evaluate**, selecting the same **operator** user
620+
621+
If `groups` is still missing, troubleshoot in this order:
622+
623+
1. **Check user membership in the correct realm**
624+
- Where: **Realm: armory** -> **Users** -> **operator** -> **Groups**
625+
- Success: `vault-operators` is listed.
626+
- Failure: No `vault-operators` membership.
627+
- Meaning: Mapper is fine, but there is no source group data to emit.
628+
629+
2. **Check mapper location for each client**
630+
- Where: **Clients** -> **vault** -> **Client scopes** -> **vault-dedicated** -> **Mappers** and **Clients** -> **agent-cli** -> **Client scopes** -> **agent-cli-dedicated** -> **Mappers**
631+
- Success: Group Membership mapper exists on both dedicated scopes.
632+
- Failure: Mapper exists only on one client, wrong scope, or at realm level only.
633+
- Meaning: Claim may appear for one client but not the other, or not appear at all.
634+
635+
3. **Check mapper fields (exact values)**
636+
- Expected: **Token Claim Name**=`groups`, **Add to access token**=ON, **Full group path**=OFF.
637+
- Success: Values match exactly.
638+
- Failure: Claim name differs (`group`, `Groups`, etc.), access token toggle OFF, or path format unexpected.
639+
- Meaning: Vault expects `groups`; name/path mismatches can make the claim appear missing or unusable.
640+
641+
4. **Regenerate token from Evaluate (do not reuse old token)**
642+
- Success: New token now contains `groups`.
643+
- Failure: Still missing in freshly generated token.
644+
- Meaning: Not a stale-session issue; configuration is still not effective.
645+
646+
5. **Clear active user sessions and test again**
647+
- Where: **Realm: armory** -> **Sessions** -> logout user sessions, then log in again.
648+
- Success: Claim appears after fresh login.
649+
- Failure: No change.
650+
- Meaning: If success, old session/token cache was the cause. If failure, continue below.
651+
652+
6. **Validate OpenBao OIDC role expects the same claim name**
653+
- Command: `podman exec -e VAULT_TOKEN=<ROOT_TOKEN> armory-vault bao read auth/oidc/role/operator`
654+
- Success: Output shows `groups_claim` set to `groups`.
655+
- Failure: `groups_claim` is different or missing.
656+
- Meaning: Keycloak may emit `groups`, but OpenBao will ignore it unless claim names match.
657+
658+
7. **Run one isolation test (temporary)**
659+
- Action: Add the same Group Membership mapper at realm scope, generate token again, then remove it after testing.
660+
- Success: Claim appears only with realm-level mapper.
661+
- Failure: Claim still absent.
662+
- Meaning: If success, dedicated scope attachment is the issue. If failure, check user/group assignment and mapper type again.
614663
---
615664

616665
### Phase 8 — Deploy the agentic layer
@@ -623,6 +672,7 @@ Once this is done you can verify the mapper is working before touching OpenBao
623672
cd vault-config/
624673
export TF_VAR_vault_token=<ROOT_TOKEN>
625674
tofu apply -var agent_enabled=true -var database_roles_enabled=true
675+
cd ..
626676
```
627677

628678
This command ensures both the agent AppRole and the database roles are enabled in Vault. Both variables must be set to `true` when deploying the agentic layer.
@@ -635,6 +685,7 @@ cp example.tfvars terraform.tfvars
635685
export TF_VAR_vault_token=<ROOT_TOKEN>
636686
tofu init
637687
tofu apply
688+
cd ../..
638689
```
639690

640691
This writes `role_id` and `wrapped_secret_id` to `/opt/armory/agent/approle/`.
@@ -647,7 +698,7 @@ python3 -m venv .venv
647698
.venv/bin/pip install -r requirements.txt
648699

649700
export VAULT_ADDR=https://127.0.0.1:8200
650-
export ARMORY_CACERT=/opt/armory/vault/tls/ca.crt
701+
export ARMORY_CACERT=/home/vagrant/project/project-armory/vault/ca-bundle.pem
651702
export APPROLE_DIR=/opt/armory/agent/approle
652703
export KEYCLOAK_URL=https://127.0.0.1:8444
653704
export OIDC_CLIENT_ID=agent-cli
@@ -668,6 +719,13 @@ cd services/agent/agent/
668719
PKCE, and submits the task in one step. The response includes a `request_id` for
669720
correlating the API log with the Vault audit log.
670721

722+
> **Headless / no-GUI environments:** `cli.py` always prints the full authorization URL
723+
> to stderr before attempting to open a browser. If no browser is available (e.g. a VM
724+
> with no desktop), copy-paste the printed URL into a browser on your host machine.
725+
> The callback listener runs on `http://127.0.0.1:18080/callback` — as long as port
726+
> 18080 is not forwarded to the host, the redirect will return to the VM's loopback
727+
> interface and the CLI will complete the exchange automatically once you log in.
728+
671729
> **Same env vars:** `cli.py` reads `KEYCLOAK_URL`, `ARMORY_CACERT`, `OIDC_CLIENT_ID`,
672730
> and `AGENT_API_URL` from the environment — the same variables set for the API above.
673731

services/agent/agent/cli.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,16 @@ def log_message(self, *args):
112112
"code_challenge_method": "S256",
113113
})
114114

115-
print("Opening browser for Keycloak login...", file=sys.stderr)
116-
webbrowser.open(auth_url)
115+
print("\nOpen the following URL in your browser to log in:\n", file=sys.stderr)
116+
print(f" {auth_url}\n", file=sys.stderr)
117+
try:
118+
if webbrowser.open(auth_url):
119+
print("(Browser opened automatically.)", file=sys.stderr)
120+
else:
121+
print("(Could not open a browser — paste the URL above manually.)", file=sys.stderr)
122+
except Exception:
123+
print("(Could not open a browser — paste the URL above manually.)", file=sys.stderr)
124+
print("Waiting for login callback on http://127.0.0.1:18080/callback ...\n", file=sys.stderr)
117125

118126
thread.join(timeout=120)
119127

0 commit comments

Comments
 (0)