diff --git a/docs/README.md b/docs/README.md index 69e353b..a4823fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -44,10 +44,15 @@ described commands or schemas are currently available. ## Guides -- [Logging in](guides/login.md) takes a first-time user from registering the - OAuth application in Asgardeo or Identity Server 7.x, through authoring the - context document and the first `wso2 login`, to a CI job that authenticates - without one. It is written to be read on its own. +- [Logging in](guides/login.md) takes a first-time user from a registered + OAuth application, through authoring the context document and the first + `wso2 login`, to a CI job that authenticates without one. Everything in it is + the same whichever product backs the deployment. +- Registering the application is product-specific, and each product has its own + walkthrough: [Asgardeo](guides/login-asgardeo.md), + [Identity Server 7.x](guides/login-identity-server.md), and + [ThunderID](guides/login-thunder.md). They are alternatives; a reader needs + exactly one, and each hands off to the login guide at its section 2. ## Research diff --git a/docs/guides/login-asgardeo.md b/docs/guides/login-asgardeo.md new file mode 100644 index 0000000..4fdc34b --- /dev/null +++ b/docs/guides/login-asgardeo.md @@ -0,0 +1,292 @@ +# Logging in with the WSO2 CLI: Asgardeo + +This is the registration walkthrough for **Asgardeo**, one of the three +deployments `wso2 login` supports. WSO2 Identity Server and ThunderID have +[their](login-identity-server.md) [own](login-thunder.md) walkthroughs, and +everything after registration — writing the context document, logging in, CI, +troubleshooting — is the same document for all three products. Read this one for +the registration, then return to +[section 2 of the login guide](login.md#2-write-the-context-document). + +**Measured against a live tenant on 2026-08-06.** The audience behaviour in +section 1 is the reason this guide exists as its own file rather than as a +variant of another product's; it is undocumented by Asgardeo, so the date is +part of the claim. + +--- + +## 1. What is different about Asgardeo + +One fact here has a consequence in every later section and in the context +document you will write, so it is worth reading before registering anything. + +**Asgardeo binds an access token's `aud` claim to the client ID, not to the API +resource whose scopes the token carries.** Measured against a live tenant on +2026-08-06: a token issued for `reference:status:read reference:status:write`, +from an application authorized against the `reference-status` API resource, +carried `"aud": ""` and nothing else. There is no setting for this. + +Two things follow: + +- **`products..audience` in your context document must be the client + ID**, not the API resource identifier, or every brokered acquisition refuses + with `auth.narrowing_unavailable`. Section 9 says it again where you record + the value. +- **The audience check cannot tell one product from another here.** It still + proves a token was minted for this client, which is what it is for; it just + cannot do more. Identity Server and Thunder both bind `aud` to the resource, + so an `audience` value is not portable between products in either direction. + The cost is recorded in + [the research document](../research/asgardeo-redirect-uri-and-scope-narrowing.md). + +Everything else the shell needs — a public client, mandatory S256 PKCE, the four +loopback callbacks, the refresh token grant, JWT access tokens — is +[listed in the login guide](login.md#1-what-the-shell-needs-from-a-deployment) +and is what the sections below configure. + +Everything that follows is in the Asgardeo console, for the organization you are +targeting. + +--- + +## 2. Create the application + +1. **Applications → New Application → Standard-Based Application**. +2. Name it something a user will recognize in a consent screen, for example + `WSO2 CLI`. +3. Protocol: **OpenID Connect**. +4. Create. + +--- + +## 3. Make it a public client with PKCE + +On the application's **Protocol** tab: + +1. Under **Allowed grant types**, select **Code** and **Refresh Token**. Clear + everything else. The refresh grant is what every later per-module acquisition + narrows from; without it a login succeeds and no module can be granted + anything. +2. Select **Public client**. This is what removes the client secret; the shell + is installed on people's machines and cannot hold one. +3. Under **PKCE**, select **Mandatory**. Leave "Support PKCE 'Plain'" + **unselected** — the shell only offers `S256`, and allowing plain would + weaken the flow without the shell ever using it. + +--- + +## 4. Register the four callback URLs + +Still on the **Protocol** tab, under **Authorized redirect URLs**, add all four +of these, one at a time: + +```text +http://127.0.0.1:10425/callback +http://127.0.0.1:10426/callback +http://127.0.0.1:10427/callback +http://127.0.0.1:10428/callback +``` + +These are the ports the shell binds, in order, taking the first that is free. +Asgardeo matches redirect URIs exactly by default, so a missing entry becomes a +mismatch error for whichever developer's machine happens to have that port busy. + +Asgardeo does in fact waive the port when matching loopback redirect URIs, the +way Identity Server 6.0.0 and later document and as RFC 8252 §7.3 asks. That was +[measured against a live tenant on 2026-08-06](../research/asgardeo-redirect-uri-and-scope-narrowing.md): +a login completed through `127.0.0.1:16000`, a port the application did not +register. + +Register all four anyway. The verdict was measured on one tenant, it is +undocumented by Asgardeo and so may change without notice, and the shell binds +only these four ports regardless — so nothing is gained by registering fewer, +and a deployment that stops waiving the port breaks every developer whose first +choice is busy. + +--- + +## 5. Add the API resource and its scopes + +The audience a module asks for is an API resource identifier, and the +permissions it asks for are that resource's scopes. + +This is two screens, not one. An API resource is an organization-level object +that many applications can share, so it is created outside your application; +authorizing it *for* your application is a separate step afterwards. + +**First, create the resource.** **API Resources** is a top-level item in the +Console's left navigation — a sibling of Applications, not a tab inside the one +you just made. + +1. **API Resources → New API Resource**. +2. Give it an **Identifier** and record it. This is the string a module's + `audience` names. It is *not* what lands in an issued token's `aud` claim on + Asgardeo — see section 1. +3. Give it a **Display Name**. This is what a user sees on a consent screen. +4. Add the scopes the module needs, for example `reference:status:read` and + `reference:status:write`. Register at least two even when the module only + uses one: the narrowing experiment works by asking for a strict subset of + what a session carries, and it has nothing to measure against a single scope. +5. The wizard's last step offers **Requires authorization**, checked by default. + **This field cannot be changed after the resource is created.** Checked means + these scopes only ever reach a token through a role. Clear it if you want the + application's own authorization to be enough on its own. Section 7 covers the + role path, which is also the way out if you left it checked. + +**Then authorize it on the application.** Back in **Applications → your +application → Authorization → Authorize resource**: select the resource, then +select its scopes. + +Watch the policy shown beside the resource on that tab. It can read +`Role Based Access Control (RBAC)` even when the resource itself did not require +authorization — the resource setting decides whether a policy is *mandatory*, +and this tab is where one is actually chosen. `No Authorization Policy` means +the scopes selected here are sufficient by themselves. Anything else means +section 7 applies, and skipping it produces a login that succeeds followed by a +refusal that names scopes rather than roles. + +--- + +## 6. Issue JWT access tokens + +On the application's **Protocol** tab, under **Access Token**, set the token type +to **JWT**. An opaque access token cannot be checked, and the broker refuses +what it cannot check. + +There is nothing to configure for the audience here, and the control that looks +like it does is not one. The **Access Token** section offers only a token type +and an attribute list; the Audience field you will find nearby belongs to **ID +Token** and does not affect access tokens. Section 1 has the measurement. + +That field sitting under the application's ID token settings is what makes this +easy to get wrong in the other direction: on Identity Server 7.3.0 the +same-looking list reaches access tokens too, so the same control does different +work on the two products. + +--- + +## 7. Create a user who can sign in, and grant it the scopes + +**The account you sign in to the Console with is not, by default, an account +your application can authenticate.** Console access and application sign-in are +two different populations: your own account administers the organization, while +what the application asks for is a user in the organization's user store. If you +signed up through Google or GitHub there is no password in that store at all, +and no amount of typing your real one will work. + +Create a user for this instead: + +1. **User Management → Users → Add User** — *Users*, not *Administrators*. +2. Give it a username or email, for example `cli-smoke@example.com`. +3. Choose to **set a password directly** rather than emailing an invitation. The + invitation path needs a working inbox, and login waits only five minutes. + +**If — and only if — section 5 left you with an authorization policy**, that +user also needs a role carrying the scopes. Authorizing the resource on the +application establishes what the application *may* ask for; under a policy it +does not establish what a user is *entitled to*, and the gap surfaces at the +first brokered acquisition as `auth.narrowing_unavailable` naming permissions. + +1. **Applications → your application → Roles → New Role**, with **Role Audience** + set to **Application**. +2. Attach the API resource and select **every** scope the context document + lists, not just the one a module uses — a session that carries less than it + later asks for cannot be narrowed. +3. Assign the user to that role, from the role's users list or from + **User Management → Users → your user → Roles**. + +A console change never reaches an existing session. Sign in again after either +step — and note that a browser SSO session will complete that sign-in without +showing you a login form, which is expected and does not mean the change was +skipped. Scopes are computed when a token is issued, not frozen into the browser +session. + +--- + +## 8. A machine-to-machine client for CI, if you need one + +A CI job has no browser and no secure store, so it uses a separate identity that +carries its own credential. Register a second application for it: + +1. **Applications → New Application → M2M Application**. +2. Grant types: **Client Credentials** only. No redirect URLs, no PKCE — there + is no browser and no user. +3. Authorize the same API resource and scopes from section 5. +4. Issue **JWT** access tokens, for the same reason as section 6. +5. Record the **client ID** and the **client secret**. + +Its `audience` follows the same rule as everything else here: the **M2M +application's own client ID**, not the API resource identifier. Under RBAC there +is one further difference from a browser login — a client-credentials grant has +no user, so a role granting the scopes must be assigned to the **application** +rather than to a person. + +[Section 5 of the login guide](login.md#5-ci-authenticate-without-a-login) has +the context document and the job wiring. + +--- + +## 9. Record what you need + +- **Client ID**. +- **Audience**, which on Asgardeo is **the client ID again** (section 1), not + the API resource identifier. +- **Scopes**, the ones you authorized on the application in section 5. +- **Issuer**, which for Asgardeo takes the shape + `https://api.asgardeo.io/t//oauth2/token`. Confirm it rather + than assuming it — fetch + `https://api.asgardeo.io/t//oauth2/token/.well-known/openid-configuration` + and use the `issuer` value verbatim. The shell discovers the token endpoint + from that document and checks that the document belongs to the issuer it was + fetched from, so a value that is close but not exact fails at login. + +--- + +## 10. Write the context document + +Everything from here is [the main login guide](login.md), from section 2. An +Asgardeo identity is `"type": "cloud"`, and its `audience` is the client ID: + +```json +{ + "name": "acme-cloud", + "type": "cloud", + "auth": { + "kind": "oauth-browser", + "issuer": "https://api.asgardeo.io/t/acme/oauth2/token", + "clientId": "REPLACE_WITH_YOUR_CLIENT_ID", + "tenant": "acme", + "credentialRef": "acme-cloud-login" + }, + "products": { + "reference": { + "endpoint": "https://api.asgardeo.io", + "audience": "REPLACE_WITH_YOUR_CLIENT_ID", + "scopes": ["reference:status:read"] + } + } +} +``` + +The example in the login guide shows the resource-identifier form of `audience`, +which is right on Identity Server and Thunder and wrong here. + +**Logging in without a browser** works on Asgardeo: add the **Device Code** +grant to the application's allowed grant types, and nothing else in the +registration changes. See +[section 3.1 of the login guide](login.md#31-logging-in-without-a-browser). + +--- + +## 11. Proving it against a live tenant + +The live runs in `test/smoke/` work against Asgardeo, and the two one-time +experiments behind `make empirical-asgardeo` are what produced the verdicts +cited above. `test/smoke/env.example` carries an Asgardeo block; fill in what +section 9 told you to record — noting that `WSO2_SMOKE_CLIENT_ID` and +`WSO2_SMOKE_AUDIENCE` are different fields that Asgardeo happens to force to the +same value — and see [`test/smoke/RUNNING.md`](../../test/smoke/RUNNING.md). + +The measured behaviour behind everything above is recorded in +[`docs/research/asgardeo-redirect-uri-and-scope-narrowing.md`](../research/asgardeo-redirect-uri-and-scope-narrowing.md) +§3, with the date and the deployment each verdict came from. diff --git a/docs/guides/login-identity-server.md b/docs/guides/login-identity-server.md new file mode 100644 index 0000000..9da3c52 --- /dev/null +++ b/docs/guides/login-identity-server.md @@ -0,0 +1,348 @@ +# Logging in with the WSO2 CLI: Identity Server 7.x + +This is the registration walkthrough for **WSO2 Identity Server 7.x**, one of +the three deployments `wso2 login` supports. Asgardeo and ThunderID have +[their](login-asgardeo.md) [own](login-thunder.md) walkthroughs, and everything +after registration — writing the context document, logging in, CI, +troubleshooting — is the same document for all three products. Read this one for +the registration, then return to +[section 2 of the login guide](login.md#2-write-the-context-document). + +**Measured against 7.3.0 on 2026-08-06.** Where this guide states what a +deployment does rather than what a control is called, that is the version it was +measured on. + +--- + +## 1. What is different about Identity Server + +Two facts here have consequences further down, so they are worth reading before +registering anything. + +**An access token's `aud` carries the API resource identifier — but only once +the resource is in the application's Audience list.** Measured against 7.3.0: + +| Application's **Audience** list | An access token's `aud` | +| --- | --- | +| empty | `""` | +| `reference-status` | `["", "reference-status"]` | + +So on Identity Server the API resource identifier *is* the right value for +`products..audience`. Leave the list empty and `aud` names the client +alone — exactly as on Asgardeo, which has no such list at all — and every +brokered acquisition refuses with `auth.narrowing_unavailable` naming the +audience. Section 7 is where you populate it. + +**The Audience field sits under the application's ID token settings on both +Identity Server and Asgardeo, and does different work on each.** On Asgardeo +that list reaches the ID token only; here it reaches both. That is what makes +this easy to get wrong in either direction, and it is why an `audience` value is +not portable between the two products. What the difference costs is recorded in +[the research document](../research/asgardeo-redirect-uri-and-scope-narrowing.md): +the broker's audience check can distinguish one product from another here, and +cannot on Asgardeo. + +Everything else the shell needs — a public client, mandatory S256 PKCE, the four +loopback callbacks, the refresh token grant, JWT access tokens — is +[listed in the login guide](login.md#1-what-the-shell-needs-from-a-deployment) +and is what the sections below configure. + +--- + +## 2. Run a deployment + +The quickest deployment to register against is a container, which images have +published for arm64 as well as amd64 since 7.2.0: + +```sh +docker run -d --name wso2is -p 9443:9443 -p 9763:9763 wso2/wso2is:7.3.0 +``` + +It answers in about a minute, with `admin` / `admin`. Nothing is persisted +outside the container, so `docker rm -f wso2is` returns the machine to where it +started — which is the reason to prefer it to an unpacked distribution for this, +where a half-registered application from a previous attempt is hard to tell from +a correct one. + +An unpacked distribution works identically. Check +`repository/conf/deployment.toml` for `offset` before assuming the ports: a +deployment with `offset = 1` answers on 9444, and the issuer you record in +section 11 has to say so. + +Everything below is in the Identity Server console, at +`https://localhost:9443/console` by default. All of it can also be done through +the management REST APIs, which accept the administrator's credentials over +basic auth — `POST /api/server/v1/api-resources`, `POST /api/server/v1/applications`, +`POST /api/server/v1/applications/{id}/authorized-apis`, `POST /scim2/Users`. +That is the better route when you expect to rebuild the deployment more than +once. + +--- + +## 3. Trust the deployment's certificate + +A default deployment serves a self-signed certificate, the shell uses the +process's ordinary HTTP client, and there is no flag anywhere in the shell for a +custom certificate authority. So until the certificate is in the OS trust store, +login cannot even reach discovery: + +```text +tls: failed to verify certificate: x509: certificate signed by unknown authority +``` + +On macOS, note that Go **ignores `SSL_CERT_FILE`** — `crypto/x509` honors it on +every Unix except Darwin — so the keychain is the only way in. Take the +certificate from the port rather than out of a keystore. A container has no +keystore on your filesystem to read, and the port is in any case the only place +that answers what the deployment actually serves: + +```sh +openssl s_client -connect localhost:9443 -servername localhost /dev/null \ + | openssl x509 -outform pem > wso2carbon-localhost.pem + +security add-trusted-cert -r trustRoot -p ssl \ + -k ~/Library/Keychains/login.keychain-db wso2carbon-localhost.pem +``` + +Use the port the deployment answers on, which is not 9443 if it carries an +offset. Against 7.3.0 this produces the same bytes as +`keytool -exportcert -alias wso2carbon -keystore repository/resources/security/wso2carbon.p12 -storepass wso2carbon` +from an unpacked distribution's root — that is the command to reach for if you +need the certificate before the deployment is running. + +**Understand what that second command grants before running it.** The default +certificate is `CA:TRUE`, and its private key ships inside every Identity Server +download and every copy of the public container image, behind the published +password `wso2carbon` — the zip and the image serve a byte-identical +certificate. Trusting it as a root means trusting a signing key that anyone can +obtain, for any hostname, not just this deployment. `-p ssl` confines it to TLS +and the login keychain confines it to your user. Remove it when the runs are +done: + +```sh +security delete-certificate -c localhost ~/Library/Keychains/login.keychain-db +``` + +The alternative, if that trade is not one you want to make even briefly, is to +replace the deployment's keypair with one whose private key only you hold. + +Thunder's equivalent trade is narrower — its certificate is generated on the +deployment that serves it — and Asgardeo needs none of this. See also +`auth.discovery_failed` in +[the login guide's troubleshooting](login.md#6-troubleshooting). + +--- + +## 4. Create the application + +1. **Applications → New Application → Standard-Based Application**. +2. Name it `WSO2 CLI`. Protocol: **OpenID Connect**. Create. + +--- + +## 5. Make it a public client with PKCE + +On the application's **Protocol** tab: + +1. **Allowed grant types**: **Code** and **Refresh Token** only. The refresh + grant is what every later per-module acquisition narrows from; without it a + login succeeds and no module can be granted anything. +2. **Public client** selected. This is what removes the client secret; the shell + is installed on people's machines and cannot hold one. +3. **PKCE Mandatory** selected, PKCE 'Plain' unselected — the shell only offers + `S256`, and allowing plain would weaken the flow without the shell ever using + it. + +--- + +## 6. Register the callback URLs + +These are the ports the shell binds, in order, taking the first that is free: + +```text +http://127.0.0.1:10425/callback +http://127.0.0.1:10426/callback +http://127.0.0.1:10427/callback +http://127.0.0.1:10428/callback +``` + +Either add the four individually, or use Identity Server's regex form as a +single entry: + +```text +regexp=(http://127.0.0.1:10425/callback|http://127.0.0.1:10426/callback|http://127.0.0.1:10427/callback|http://127.0.0.1:10428/callback) +``` + +Identity Server waives the port when matching loopback redirect URIs from 6.0.0 +onwards, so a single `http://127.0.0.1:10425/callback` entry is enough. Measured +on 7.3.0, the waiver is stronger than the documentation implies: a login through +`127.0.0.1:16000` completed against the regexp above, which enumerates four +ports and does not include that one. Loopback flexibility is applied ahead of +the registered pattern rather than as a fallback when none matches. + +Register all four anyway. It keeps the same configuration valid on Asgardeo, and +it keeps the registration honest about which ports the shell actually binds. + +--- + +## 7. Add the API resource and its scopes + +The audience a module asks for is an API resource identifier, and the +permissions it asks for are that resource's scopes. + +This is two screens, not one. An API resource is a server-level object that many +applications can share, so it is created outside your application; authorizing +it *for* your application is a separate step afterwards. + +**First, create the resource.** **API Resources → New API Resource**. + +1. Give it an **Identifier** and record it. This is the string a module's + `audience` names, and — unlike on Asgardeo — it is also what an issued + token's `aud` will carry, once section 1's Audience list is populated below. +2. Add the scopes the module needs, for example `reference:status:read` and + `reference:status:write`. Register at least two even when the module only + uses one: the narrowing experiment works by asking for a strict subset of + what a session carries, and it has nothing to measure against a single scope. +3. **Requires authorization** cannot be changed after the resource is created. + Checked means these scopes only ever reach a token through a role. Section 9 + covers the role path, which is also the way out if you left it checked. + +**Then authorize it on the application**, on the application's **API +Authorization** tab: select the resource, then select its scopes. + +**Then add the audience, which is the step that makes the difference.** On the +application's **Protocol** tab, find **Audience** and add the API resource +identifier. Section 1 has the measurement: without this the token's `aud` names +the client alone and every brokered acquisition refuses. + +--- + +## 8. Issue JWT access tokens + +Identity Server issues JWT access tokens by default. If the deployment has been +changed to opaque, change it back for this application: an opaque access token +cannot be checked, and the broker refuses what it cannot check. + +--- + +## 9. Create a user who can sign in, and grant it the scopes + +**The account you sign in to the Console with is not, by default, an account +your application can authenticate.** Console access and application sign-in are +two different populations: the administrator account administers the server, +while what the application asks for is a user in the user store. + +Create a user for this instead, under **User Management → Users**, and set its +password directly rather than emailing an invitation — the invitation path needs +a working inbox, and login waits only five minutes. + +**If — and only if — section 7 left you with an authorization policy**, that +user also needs a role carrying the scopes. Authorizing the resource on the +application establishes what the application *may* ask for; under a policy it +does not establish what a user is *entitled to*, and the gap surfaces at the +first brokered acquisition as `auth.narrowing_unavailable` naming permissions. +Create a role with the application as its audience, attach the API resource and +select **every** scope the context document lists — not just the one a module +uses, because a session that carries less than it later asks for cannot be +narrowed — then assign the user to it. + +The reasoning is identical to Asgardeo's and only the console differs; if a +control is not where this says, +[the Asgardeo walkthrough](login-asgardeo.md#7-create-a-user-who-can-sign-in-and-grant-it-the-scopes) +names the equivalent screens in more detail. + +A console change never reaches an existing session. Sign in again after either +step — and note that a browser SSO session will complete that sign-in without +showing you a login form, which is expected and does not mean the change was +skipped. Scopes are computed when a token is issued, not frozen into the browser +session. + +--- + +## 10. A confidential client for CI, if you need one + +A CI job has no browser and no secure store, so it uses a separate identity that +carries its own credential. Register a second application for it: + +1. A standard-based application with the **Client Credentials** grant and **no** + public-client setting. +2. No redirect URLs, no PKCE — there is no browser and no user. +3. Authorize the same API resource and scopes from section 7, and add the + resource to this application's **Audience** list too. +4. Issue **JWT** access tokens, for the same reason as section 8. +5. Record the **client ID** and the **client secret**. + +Under RBAC there is one further difference from a browser login — a +client-credentials grant has no user, so a role granting the scopes must be +assigned to the **application** rather than to a person. + +[Section 5 of the login guide](login.md#5-ci-authenticate-without-a-login) has +the context document and the job wiring. + +--- + +## 11. Record what you need + +- **Client ID**. +- **Audience**, which is the **API resource identifier** from section 7 — and + which only reaches `aud` because you added it to the application's Audience + list. This is not the same value as the client ID, and it is not what an + Asgardeo deployment wants. +- **Scopes**, the ones you authorized on the application in section 7. +- **Issuer**, which for a default 7.x deployment takes the shape + `https://localhost:9443/oauth2/token`. Confirm it from + `https://localhost:9443/oauth2/token/.well-known/openid-configuration` and use + the `issuer` value verbatim. A deployment carrying an offset answers on + another port, and the port is part of the issuer. +- **Whether this machine trusts the deployment's certificate** (section 3). + +--- + +## 12. Write the context document + +Everything from here is [the main login guide](login.md), from section 2. An +Identity Server identity is `"type": "onprem"`, and its `audience` is the API +resource identifier: + +```json +{ + "name": "is-local", + "type": "onprem", + "auth": { + "kind": "oauth-browser", + "issuer": "https://localhost:9443/oauth2/token", + "clientId": "REPLACE_WITH_YOUR_CLIENT_ID", + "credentialRef": "is-local-login" + }, + "products": { + "reference": { + "endpoint": "https://localhost:9443", + "audience": "reference-status", + "scopes": ["reference:status:read"] + } + } +} +``` + +**Logging in without a browser** works on Identity Server 7.x: add the **Device +Code** grant to the application's allowed grant types, and nothing else in the +registration changes. See +[section 3.1 of the login guide](login.md#31-logging-in-without-a-browser). + +--- + +## 13. Proving it against this deployment + +The live runs in `test/smoke/` work against Identity Server exactly as they do +against the other two products. `test/smoke/env.example` carries an Identity +Server block; fill in what section 11 told you to record and see +[`test/smoke/RUNNING.md`](../../test/smoke/RUNNING.md). + +`WSO2_SMOKE_CLIENT_ID` and `WSO2_SMOKE_AUDIENCE` **differ here**, where Asgardeo +forces them to the same value. Copying one deployment's env file to another and +changing only the issuer is therefore the mistake to expect; it costs a browser +sign-in and ends in `auth.narrowing_unavailable` naming the audience. + +The measured behaviour behind everything above is recorded in +[`docs/research/asgardeo-redirect-uri-and-scope-narrowing.md`](../research/asgardeo-redirect-uri-and-scope-narrowing.md) +§3.1, with the date and the deployment each verdict came from. diff --git a/docs/guides/login-thunder.md b/docs/guides/login-thunder.md index 0b8df49..49aff53 100644 --- a/docs/guides/login-thunder.md +++ b/docs/guides/login-thunder.md @@ -1,17 +1,12 @@ # Logging in with the WSO2 CLI: ThunderID -This is the registration walkthrough for **ThunderID**, the third deployment -`wso2 login` supports. Asgardeo and WSO2 Identity Server are covered by -[the main login guide](login.md), sections 2 and 3, and everything after -registration — writing the context document, logging in, CI, troubleshooting — -is the same document for all three products. Read this one for the registration, -then return to [section 4 of the login guide](login.md#4-write-the-context-document). - -It is a separate file for two reasons. Thunder is pre-1.0 and will change faster -than the other two, and churn is cheapest in a file nothing cross-references. -And the login guide is already long enough that a third walkthrough inside it -would push it past thirteen hundred lines. Splitting all three product -walkthroughs symmetrically is [tracked separately](https://github.com/wso2/wso2-cli/issues/45). +This is the registration walkthrough for **ThunderID**, one of the three +deployments `wso2 login` supports. Asgardeo and WSO2 Identity Server have +[their](login-asgardeo.md) [own](login-identity-server.md) walkthroughs, and +everything after registration — writing the context document, logging in, CI, +troubleshooting — is the same document for all three products. Read this one for +the registration, then return to +[section 2 of the login guide](login.md#2-write-the-context-document). **Written against ThunderID `v1.0.0-beta`.** Console layouts move in an alpha and beta product; if a control named here is not where this says, the version @@ -268,6 +263,44 @@ The client-credentials grant has no earlier authorization to inherit a resource binding from, so the shell sends the resource indicator on that request too. A Thunder deployment refuses the grant outright without it. +**That is why a Thunder CI identity carries `provider` exactly as a browser one +does.** `provider` is what selects the resource-bound derivation, and the shell +sends no indicator without it — so the +[CI context in the login guide](login.md#51-write-the-ci-context), which is +written for Asgardeo, cannot be used here as it stands. This is the Thunder +form: + +```json +{ + "name": "thunder-ci", + "type": "onprem", + "auth": { + "kind": "client-credentials", + "provider": "thunder", + "issuer": "https://localhost:8090", + "clientId": "REPLACE_WITH_YOUR_CI_CLIENT_ID", + "clientSecretVariable": "WSO2_THUNDER_CI_SECRET" + }, + "products": { + "reference": { + "endpoint": "https://localhost:8090", + "audience": "https://localhost:8090/reference-status", + "scopes": ["read", "write"] + } + } +} +``` + +The same two rules that bind a Thunder browser identity bind this one, and the +shell refuses the document rather than the grant if either is broken: **exactly +one product**, and an **audience that is an absolute URI**. Carrying the login +guide's `"audience": "reference-status"` over is refused at parse as not a URI, +which is the cheap failure; omitting `provider` is the expensive one, because +the document parses and the deployment refuses every grant. + +[Section 5.2 of the login guide](login.md#52-wire-the-job) has the job wiring, +which is the same for all three products. + --- ## 8. Record what you need @@ -284,7 +317,7 @@ Thunder deployment refuses the grant outright without it. ## 9. Write the context document -Everything from here is [the main login guide](login.md), from section 4. Two +Everything from here is [the main login guide](login.md), from section 2. Two members are Thunder-specific: ```json diff --git a/docs/guides/login.md b/docs/guides/login.md index adf5c36..9ad162b 100644 --- a/docs/guides/login.md +++ b/docs/guides/login.md @@ -1,26 +1,27 @@ # Logging in with the WSO2 CLI **Status:** Working draft -**Last reviewed:** 2026-08-06 +**Last reviewed:** 2026-08-10 **Related:** [Architecture](../architecture.md), [product requirements](../product-requirements.md), [shell commands](../reference/commands.md), [authentication context examples](../examples/authentication-contexts.md) -This guide takes you from an empty deployment to a working `wso2 login` and a CI -job that authenticates without one. It is written to be read on its own: every -value you need to produce is produced here, and nothing below asks you to go and -read another document first. +This guide takes you from a registered application to a working `wso2 login` and +a CI job that authenticates without one. Everything here is the same whichever +product backs your deployment. -Two audiences, one path. Sections 2 and 3 are alternatives — register the -application in Asgardeo **or** in Identity Server 7.x, whichever you are -targeting — and everything after them is the same for both. +**Registering the application is product-specific, and each product has its own +walkthrough.** Read one of these first, then come back here at section 2: -Registering against **ThunderID** is a third alternative, and it lives in -[its own walkthrough](login-thunder.md) because Thunder decides an access -token's audience differently enough to change what you register and what you -write down. Read that instead of sections 2 and 3, then rejoin this guide at -section 4. +| Deployment | Walkthrough | +| --- | --- | +| **Asgardeo** | [Registering in Asgardeo](login-asgardeo.md) | +| **WSO2 Identity Server 7.x** | [Registering in Identity Server](login-identity-server.md) | +| **ThunderID** | [Registering in ThunderID](login-thunder.md) | + +They are alternatives — you need exactly one. Each is written to be read on its +own, and each ends by handing you the four values section 2 asks for. --- @@ -31,9 +32,8 @@ keeps the resulting refresh token in the operating system's secure store, and derives a separate short-lived access token for each module that asks for one. Nothing else is stored, and no module ever sees the session. -That design imposes five requirements on the application you are about to -register. Each one has a section below; this list is here so you know what the -clicking is for. +That design imposes five requirements on the application you register. This list +is here so you know what the clicking in your product's walkthrough is for. 1. **A public client.** No client secret. The shell is installed on people's machines, so it cannot hold one, and PKCE is what replaces it. @@ -58,342 +58,27 @@ clicking is for. the token a module receives carries exactly the permissions that module asked for and is bound to the audience it asked for. It cannot prove that about an opaque token, and it refuses rather than hand over a grant it could not - check. See `auth.narrowing_unavailable` in section 8. + check. See `auth.narrowing_unavailable` in section 6. A sixth is optional and needed only for logging in from a machine with no -browser: **the device code grant**. Section 5.1 covers it, and nothing else in +browser: **the device code grant**. Section 3.1 covers it, and nothing else in the registration changes. ---- - -## 2. Register the application in Asgardeo - -In the Asgardeo console, for the organization you are targeting. - -### 2.1 Create the application - -1. **Applications → New Application → Standard-Based Application**. -2. Name it something a user will recognize in a consent screen, for example - `WSO2 CLI`. -3. Protocol: **OpenID Connect**. -4. Create. - -### 2.2 Make it a public client with PKCE - -On the application's **Protocol** tab: - -1. Under **Allowed grant types**, select **Code** and **Refresh Token**. Clear - everything else. -2. Select **Public client**. This is what removes the client secret; the shell - cannot present one. -3. Under **PKCE**, select **Mandatory**. Leave "Support PKCE 'Plain'" - **unselected** — the shell only offers `S256`, and allowing plain would - weaken the flow without the shell ever using it. - -### 2.3 Register the four callback URLs - -Still on the **Protocol** tab, under **Authorized redirect URLs**, add all four -of the URLs listed in section 1.3, one at a time. Asgardeo matches redirect URIs -exactly by default, so a missing entry becomes a mismatch error for whichever -developer's machine happens to have that port busy. - -Asgardeo does in fact waive the port when matching loopback redirect URIs, the -way Identity Server 6.0.0 and later document and as RFC 8252 §7.3 asks. That was -[measured against a live tenant on 2026-08-06](../research/asgardeo-redirect-uri-and-scope-narrowing.md): -a login completed through `127.0.0.1:16000`, a port the application did not -register. - -Register all four anyway. The verdict was measured on one tenant, it is -undocumented by Asgardeo and so may change without notice, and the shell binds -only these four ports regardless — so nothing is gained by registering fewer, -and a deployment that stops waiving the port breaks every developer whose first -choice is busy. - -### 2.4 Add the API resource and its scopes - -The audience a module asks for is an API resource identifier, and the -permissions it asks for are that resource's scopes. - -This is two screens, not one. An API resource is an organization-level object -that many applications can share, so it is created outside your application; -authorizing it *for* your application is a separate step afterwards. - -**First, create the resource.** **API Resources** is a top-level item in the -Console's left navigation — a sibling of Applications, not a tab inside the one -you just made. - -1. **API Resources → New API Resource**. -2. Give it an **Identifier** and record it. This is the string a module's - `audience` names. Read section 2.5 before assuming it is also what lands in - an issued token's `aud` claim on Asgardeo — it is not. -3. Give it a **Display Name**. This is what a user sees on a consent screen. -4. Add the scopes the module needs, for example `reference:status:read` and - `reference:status:write`. Register at least two even when the module only - uses one: the narrowing experiment in section 9 works by asking for a strict - subset of what a session carries, and it has nothing to measure against a - single scope. -5. The wizard's last step offers **Requires authorization**, checked by default. - **This field cannot be changed after the resource is created.** Checked means - these scopes only ever reach a token through a role. Clear it if you want the - application's own authorization to be enough on its own. Section 2.7 covers - the role path, which is also the way out if you left it checked. - -**Then authorize it on the application.** Back in **Applications → your -application → Authorization → Authorize resource**: select the resource, then -select its scopes. - -Watch the policy shown beside the resource on that tab. It can read -`Role Based Access Control (RBAC)` even when the resource itself did not require -authorization — the resource setting decides whether a policy is *mandatory*, -and this tab is where one is actually chosen. `No Authorization Policy` means -the scopes selected here are sufficient by themselves. Anything else means -section 2.7 applies, and skipping it produces a login that succeeds followed by -a refusal that names scopes rather than roles. - -### 2.5 Issue JWT access tokens, and know what `aud` will say - -On the application's **Protocol** tab, under **Access Token**, set the token type -to **JWT**. An opaque access token cannot be checked, and the broker refuses -what it cannot check. - -**Asgardeo binds an access token's `aud` claim to the client ID, not to the API -resource whose scopes the token carries.** Measured against a live tenant on -2026-08-06: a token issued for `reference:status:read reference:status:write`, -from an application authorized against the `reference-status` API resource, -carried `"aud": ""` and nothing else. There is no setting for this. -The **Access Token** section offers only a token type and an attribute list; the -Audience field you will find nearby belongs to **ID Token** and does not affect -access tokens. - -So on Asgardeo, `products..audience` in your context document must be -**the client ID**, not the API resource identifier, or every brokered -acquisition refuses with `auth.narrowing_unavailable`. Section 4.3 says the same -where the field is defined, and the consequence is recorded in -[the research document](../research/asgardeo-redirect-uri-and-scope-narrowing.md): -the audience check still proves a token was minted for this client, but it -cannot distinguish one product from another. - -Identity Server 7.3.0 does **not** behave this way — the same-looking Audience -field there reaches access tokens too, so the resource identifier is the right -value on that product. Section 3.5 has the measurement. Do not carry an -Asgardeo `audience` over to an Identity Server deployment, or the reverse. - -### 2.6 Record what you need - -From the **Protocol** and **Info** tabs: - -- **Client ID**. -- **Issuer**, which for Asgardeo takes the shape - `https://api.asgardeo.io/t//oauth2/token`. Confirm it rather - than assuming it — fetch - `https://api.asgardeo.io/t//oauth2/token/.well-known/openid-configuration` - and use the `issuer` value verbatim. The shell discovers the token endpoint - from that document and checks that the document belongs to the issuer it was - fetched from, so a value that is close but not exact fails at login. - -### 2.7 Create a user who can sign in, and grant it the scopes - -**The account you sign in to the Console with is not, by default, an account -your application can authenticate.** Console access and application sign-in are -two different populations: your own account administers the organization, while -what the application asks for is a user in the organization's user store. If you -signed up through Google or GitHub there is no password in that store at all, -and no amount of typing your real one will work. - -Create a user for this instead: - -1. **User Management → Users → Add User** — *Users*, not *Administrators*. -2. Give it a username or email, for example `cli-smoke@example.com`. -3. Choose to **set a password directly** rather than emailing an invitation. The - invitation path needs a working inbox, and login waits only five minutes. - -**If — and only if — section 2.4 left you with an authorization policy**, that -user also needs a role carrying the scopes. Authorizing the resource on the -application establishes what the application *may* ask for; under a policy it -does not establish what a user is *entitled to*, and the gap surfaces at the -first brokered acquisition as `auth.narrowing_unavailable` naming permissions. - -1. **Applications → your application → Roles → New Role**, with **Role Audience** - set to **Application**. -2. Attach the API resource and select **every** scope the context document - lists, not just the one a module uses — a session that carries less than it - later asks for cannot be narrowed. -3. Assign the user to that role, from the role's users list or from - **User Management → Users → your user → Roles**. - -A console change never reaches an existing session. Sign in again after either -step — and note that a browser SSO session will complete that sign-in without -showing you a login form, which is expected and does not mean the change was -skipped. Scopes are computed when a token is issued, not frozen into the browser -session. - ---- - -## 3. Register the application in Identity Server 7.x - -The quickest deployment to register against is a container, which images have -published for arm64 as well as amd64 since 7.2.0: - -```sh -docker run -d --name wso2is -p 9443:9443 -p 9763:9763 wso2/wso2is:7.3.0 -``` - -It answers in about a minute, with `admin` / `admin`. Nothing is persisted -outside the container, so `docker rm -f wso2is` returns the machine to where it -started — which is the reason to prefer it to an unpacked distribution for this, -where a half-registered application from a previous attempt is hard to tell from -a correct one. - -An unpacked distribution works identically. Check `repository/conf/deployment.toml` -for `offset` before assuming the ports: a deployment with `offset = 1` answers -on 9444, and section 3.6 is where that matters. - -Everything below is in the Identity Server console, at -`https://localhost:9443/console` by default. All of it can also be done through -the management REST APIs, which accept the administrator's credentials over -basic auth — `POST /api/server/v1/api-resources`, `POST /api/server/v1/applications`, -`POST /api/server/v1/applications/{id}/authorized-apis`, `POST /scim2/Users`. -That is the better route when you expect to rebuild the deployment more than -once. - -### 3.1 Create the application - -1. **Applications → New Application → Standard-Based Application**. -2. Name it `WSO2 CLI`. Protocol: **OpenID Connect**. Create. - -### 3.2 Make it a public client with PKCE - -On the **Protocol** tab: grant types **Code** and **Refresh Token** only; -**Public client** selected; **PKCE Mandatory** selected; PKCE 'Plain' -unselected. Same reasoning as section 2.2. - -### 3.3 Register the callback URLs - -Either add the same four URLs from section 1.3 individually, or use Identity -Server's regex form as a single entry: - -``` -regexp=(http://127.0.0.1:10425/callback|http://127.0.0.1:10426/callback|http://127.0.0.1:10427/callback|http://127.0.0.1:10428/callback) -``` - -Identity Server waives the port when matching loopback redirect URIs from 6.0.0 -onwards, so a single `http://127.0.0.1:10425/callback` entry is enough there. -Measured on 7.3.0, the waiver is stronger than the documentation implies: a -login through `127.0.0.1:16000` completed against the regexp above, which -enumerates four ports and does not include that one. Loopback flexibility is -applied ahead of the registered pattern rather than as a fallback when none -matches. - -Register all four anyway. It keeps the same configuration valid on Asgardeo, and -it keeps the registration honest about which ports the shell actually binds. - -### 3.4 Add the API resource and its scopes - -**API Resources → New API Resource**, with an identifier and scopes as in -section 2.4, then authorize it on the application's **API Authorization** tab. -Section 2.4's two warnings apply here too: the resource is created on a -different screen than the one that authorizes it, and the **Requires -authorization** setting cannot be changed afterwards. - -### 3.5 Issue JWT access tokens, and add the audience - -Identity Server issues JWT access tokens by default. If the deployment has been -changed to opaque, change it back for this application — see section 2.5 for -why. - -**Identity Server does not behave like Asgardeo here, and this is the step that -makes the difference.** Measured against 7.3.0 on 2026-08-06: - -| Application's **Audience** list | An access token's `aud` | -| --- | --- | -| empty | `""` | -| `reference-status` | `["", "reference-status"]` | - -So on Identity Server the API resource identifier *is* the right value for -`products..audience` — but only once it is in that list. Leave the -list empty and `aud` names the client alone, exactly as on Asgardeo, and every -brokered acquisition refuses with `auth.narrowing_unavailable` naming the -audience. - -On the application's **Protocol** tab, find **Audience** and add the API -resource identifier from section 3.4. - -The field sits under the application's ID token settings on both products, which -is what makes this easy to get wrong in the other direction: on Asgardeo that -list reaches the ID token only, and on Identity Server 7.3.0 it reaches both. -The same-looking control does different work. Section 2.5 records the Asgardeo -side, and -[the research document](../research/asgardeo-redirect-uri-and-scope-narrowing.md) -records what the difference costs — the broker's audience check can distinguish -one product from another on Identity Server, and cannot on Asgardeo. - -### 3.6 Record what you need - -- **Client ID**. -- **Issuer**, which for a default Identity Server 7.x deployment takes the shape - `https://localhost:9443/oauth2/token`. Confirm it from - `https://localhost:9443/oauth2/token/.well-known/openid-configuration` and use - the `issuer` value verbatim. -- **Whether this machine trusts the deployment's TLS certificate.** A default - deployment serves a self-signed one, the shell uses the process's ordinary - HTTP client, and there is no flag anywhere in the shell for a custom CA. So - until the certificate is in the OS trust store, login cannot even reach - discovery: - - ``` - tls: failed to verify certificate: x509: certificate signed by unknown authority - ``` - - On macOS, note that Go **ignores `SSL_CERT_FILE`** — `crypto/x509` honors it - on every Unix except Darwin — so the keychain is the only way in. Take the - certificate from the port rather than out of a keystore. A container has no - keystore on your filesystem to read, and the port is in any case the only - place that answers what the deployment actually serves: - - ```sh - openssl s_client -connect localhost:9443 -servername localhost /dev/null \ - | openssl x509 -outform pem > wso2carbon-localhost.pem - - security add-trusted-cert -r trustRoot -p ssl \ - -k ~/Library/Keychains/login.keychain-db wso2carbon-localhost.pem - ``` - - Use the port the deployment answers on, which is not 9443 if it carries an - offset. Against 7.3.0 this produces the same bytes as - `keytool -exportcert -alias wso2carbon -keystore repository/resources/security/wso2carbon.p12 -storepass wso2carbon` - from an unpacked distribution's root — that is the command to reach for if you - need the certificate before the deployment is running. - - **Understand what that second command grants before running it.** The default - certificate is `CA:TRUE`, and its private key ships inside every Identity - Server download and every copy of the public container image, behind the - published password `wso2carbon` — the zip and the image serve a byte-identical - certificate. Trusting it as a root means trusting a signing key that anyone - can obtain, for any hostname, not just this deployment. `-p ssl` confines it - to TLS and the login keychain confines it to your user. Remove it when the - runs are done: - - ```sh - security delete-certificate -c localhost ~/Library/Keychains/login.keychain-db - ``` - - The alternative, if that trade is not one you want to make even briefly, is to - replace the deployment's keypair with one whose private key only you hold. - - See also `auth.discovery_failed` in section 8. - -You also need a user to sign in as, and possibly a role granting the scopes. -Section 2.7 describes both; the reasoning is identical on Identity Server, only -the console differs. +**Where the products differ is the fifth requirement**, and the difference +decides what you write as `audience` in section 2. Asgardeo binds an access +token's `aud` to the client ID; Identity Server binds it to the API resource +identifier, once that is in the application's audience list; Thunder names a +*resource server* per request and calls the object something else again. Each +walkthrough states its product's answer and shows the measurement behind it. --- -## 4. Write the context document +## 2. Write the context document The shell reads contexts and never writes them, so this file is authored by hand. -### 4.1 Where it goes +### 2.1 Where it goes ``` ~/.wso2/cli/contexts.json @@ -406,7 +91,7 @@ the file then lives at `$WSO2_HOME/cli/contexts.json`. mkdir -p ~/.wso2/cli ``` -### 4.2 What it says +### 2.2 What it says A context document names **identities** — how to authenticate, and what each identity may reach — and **contexts**, which select an identity and the @@ -450,19 +135,24 @@ Copy this, then replace the four values marked in the comments below it: Replace: -- `issuer` — the value you confirmed in section 2.6 or 3.6. +- `issuer` — the value your walkthrough had you confirm against the deployment's + own discovery document. - `clientId` — the client ID you recorded. -- `audience` — **on Asgardeo, the client ID again**, because that is the only - value Asgardeo puts in an access token's `aud` claim (section 2.5). On a - deployment that binds tokens to API resources, the resource identifier from - section 2.4 or 3.4. The example above shows the resource-identifier form, so - against Asgardeo it needs the client ID substituted here. +- `audience` — **the value your product's walkthrough told you to record**, and + the one field where copying another product's document goes wrong. It is the + client ID on [Asgardeo](login-asgardeo.md#1-what-is-different-about-asgardeo), + the API resource identifier on + [Identity Server](login-identity-server.md#1-what-is-different-about-identity-server), + and an absolute resource-server URI on + [Thunder](login-thunder.md#1-what-is-different-about-thunder). The example + above shows the resource-identifier form, so against Asgardeo it needs the + client ID substituted here. - `scopes` — the scopes you authorized on the application. -For an Identity Server deployment, also set `"type": "onprem"` and use the -`https://localhost:9443/oauth2/token` issuer shape. +Each walkthrough's last-but-one section shows the whole identity block filled in +for that product, including `type` and any product-specific member. -### 4.3 What each field means +### 2.3 What each field means | Field | Meaning | | --- | --- | @@ -470,18 +160,19 @@ For an Identity Server deployment, also set `"type": "onprem"` and use the | `defaultContext` | The context used when no `--context` flag and no `WSO2_CONTEXT` is given. Must name a context declared below. | | `identities[].name` | Lower-case letters, digits and dashes, starting with a letter, up to 64 characters. | | `identities[].type` | `cloud` or `onprem`. Nothing else is accepted. | -| `auth.kind` | `oauth-browser` for a person at a browser. `oauth-device` for an identity that can only be established without one — see section 5.1. `client-credentials` for CI — see section 7. `pat` is named by the schema but not implemented in this release. | +| `auth.kind` | `oauth-browser` for a person at a browser. `oauth-device` for an identity that can only be established without one — see section 3.1. `client-credentials` for CI — see section 5. `pat` is named by the schema but not implemented in this release. | | `auth.issuer` | The issuer, verbatim from its discovery document. | | `auth.clientId` | The registered public client. | | `auth.tenant` | The identity's home organization. | +| `auth.provider` | Names the product when the shell must ask it for tokens in a product-specific shape. Required for Thunder; see [its walkthrough](login-thunder.md#9-write-the-context-document). | | `auth.credentialRef` | The name the session is stored under in the OS secure store. **Required** for `oauth-browser` and `oauth-device`; **not allowed** for `client-credentials`. Same character rules as an identity name. | | `products.` | What this identity may reach for one module. The namespace is the module's own name, and follows the same character rules as an identity name. | | `products..endpoint` | The product's base URL. **Required** on every product entry, and must be an absolute `http` or `https` URL with a host. | -| `products..audience` | What the issued token's `aud` claim must carry. A module asking for any other audience is refused. Conceptually this is the API resource identifier — but on Asgardeo it must be **the client ID**, because that is the only thing Asgardeo puts in `aud`. See section 2.5. | +| `products..audience` | What the issued token's `aud` claim must carry. A module asking for any other audience is refused. Which value that is differs by product — see section 2.2. | | `products..scopes` | The permissions this identity carries. A module asking for one that is not listed is refused. | -| `contexts[].organization` | The organization to act within. Either leave it out, or set it to the identity's `auth.tenant` — this release cannot switch a session out of its home tenant, and any other value is refused. See `auth.organization_switch_unsupported` in section 8. | +| `contexts[].organization` | The organization to act within. Either leave it out, or set it to the identity's `auth.tenant` — this release cannot switch a session out of its home tenant, and any other value is refused. See `auth.organization_switch_unsupported` in section 6. | -### 4.4 Check it +### 2.4 Check it ```sh wso2 login --context acme-dev @@ -491,7 +182,7 @@ If the document is malformed, the shell says so before opening any browser. --- -## 5. Log in +## 3. Log in ```sh wso2 login @@ -526,7 +217,7 @@ a missing desktop. The command waits up to five minutes for you. -## 5.1 Logging in without a browser +## 3.1 Logging in without a browser If the machine you are typing on has no browser that can reach it — you are over SSH, or inside a container — the login above cannot finish. It waits for the @@ -548,8 +239,8 @@ release**. Until it arrives, the way to have both is two identities — one `oauth-browser`, one `oauth-device` — with different `credentialRef` values, and a context for each. -**What to register.** Everything from section 2 or 3 applies unchanged, with two -differences: +**What to register.** Everything in your product's walkthrough applies +unchanged, with two differences: - Add the **Device Code** grant to the application's allowed grant types. Asgardeo and Identity Server 7.x both support it; on Asgardeo it appears in @@ -561,7 +252,7 @@ Thunder-backed products cannot use this flow at all — Thunder registers no device grant handler, so its deployments advertise none and the shell refuses before printing anything. -**The context document** is the section 4.2 document with one word changed: +**The context document** is the section 2.2 document with one word changed: ```json "auth": { @@ -614,7 +305,7 @@ command afterwards behaves identically. --- -## 6. What login stored, and where +## 4. What login stored, and where - **The refresh token** goes to the operating system's secure store — Keychain on macOS, Secret Service on Linux, Credential Manager on Windows — under the @@ -634,28 +325,23 @@ To sign out, delete the secure-store entry named by `credentialRef`. --- -## 7. CI: authenticate without a login +## 5. CI: authenticate without a login A CI job has no browser and no secure store, so it does not use a session at all. It uses a machine-to-machine identity that carries its own credential and exchanges it inline, on every command. **There is no login step in CI** — a job that runs `wso2 login` is refused with `auth.login_not_required`. -### 7.1 Register the machine-to-machine application +**Register the machine-to-machine application first.** That is product-specific, +and each walkthrough has a section for it: +[Asgardeo](login-asgardeo.md#8-a-machine-to-machine-client-for-ci-if-you-need-one), +[Identity Server](login-identity-server.md#10-a-confidential-client-for-ci-if-you-need-one), +[Thunder](login-thunder.md#7-a-confidential-client-for-ci-if-you-need-one). All +three come down to the same thing: the **Client Credentials** grant and nothing +else, no redirect URLs, no PKCE, the same API resource and scopes as the browser +application, JWT access tokens, and a recorded client ID and secret. -In Asgardeo: **Applications → New Application → M2M Application**. In Identity -Server: a standard-based application with the **Client Credentials** grant and -**no** public-client setting. - -Then, for either: - -- Grant types: **Client Credentials** only. No redirect URLs, no PKCE — there is - no browser and no user. -- Authorize the same API resource and scopes from section 2.4 or 3.4. -- Issue **JWT** access tokens, for the same reason as section 2.5. -- Record the **client ID** and the **client secret**. - -### 7.2 Write the CI context +### 5.1 Write the CI context ```json { @@ -691,7 +377,7 @@ Then, for either: } ``` -Two differences from section 4.2, and the schema enforces both: +Two differences from section 2.2, and the schema enforces both: - `clientSecretVariable` **replaces** `credentialRef`. It names an environment variable; it is not the secret. Upper-case letters, digits and underscores, @@ -701,13 +387,22 @@ Two differences from section 4.2, and the schema enforces both: The secret itself never goes in this file, and the file is safe to commit. -`audience` follows the same rule as section 4.2: on Asgardeo it must be the M2M -application's own client ID, not the API resource identifier the example shows. -Under RBAC there is one further difference from a browser login — a -client-credentials grant has no user, so a role granting the scopes must be -assigned to the **application** rather than to a person. +**The example above is an Asgardeo identity, and two of its members are +product-specific.** Substitute both before using it against another deployment: + +- `audience` follows the same per-product rule as section 2.2, applied to *this* + application. On Asgardeo it must be the M2M application's own client ID, not + the API resource identifier the example shows. +- `auth.provider` carries into a CI identity exactly as it does a browser one. + A Thunder deployment needs `"provider": "thunder"` here, because that is what + makes the shell name the protected resource on the client-credentials request + — and Thunder refuses a grant that names none. The document parses either + way, so leaving it out fails at the first command rather than at the first + read. [The Thunder walkthrough](login-thunder.md#7-a-confidential-client-for-ci-if-you-need-one) + shows the whole identity, including the two further rules a resource-bound + identity must satisfy. -### 7.3 Wire the job +### 5.2 Wire the job The secret comes from the CI system's own secret store into the named variable. Nothing else changes; there is no login step. @@ -754,17 +449,20 @@ one grant, is never written to the state root, and is never passed to a module. --- -## 8. Troubleshooting +## 6. Troubleshooting -Every refusal the shell makes carries a typed code. Find yours here. +Every refusal the shell makes carries a typed code. Find yours here. This table +covers all three products; failures that can only happen on one of them are in +that product's walkthrough — [Thunder's](login-thunder.md#10-troubleshooting) is +the longest, because its registration model differs the most. ### The context document: `contexts.*` -These come from the file you wrote in section 4, and they are the ones a +These come from the file you wrote in section 2, and they are the ones a first-time user meets most often. None of them reaches a browser. - **`contexts.document_malformed`** — the document was read but is not valid. - The message names the specific defect, and section 4.3 is the field-by-field + The message names the specific defect, and section 2.3 is the field-by-field reference for it. The usual causes are a name that breaks the character rules (identity names, context names and `credentialRef` are lower-case letters, digits and dashes, starting with a letter), a `type` that is not exactly @@ -782,12 +480,12 @@ first-time user meets most often. None of them reaches a browser. ### `auth.context_not_selected` There is no context document at all, or it declares no context to select. -Create `~/.wso2/cli/contexts.json` as in section 4. +Create `~/.wso2/cli/contexts.json` as in section 2. ### `shell.unknown_command` The first word was not `help`, `login` or `version`, and no installed module -owns that namespace. See the caveat at the end of section 7.3. +owns that namespace. See the caveat at the end of section 5.2. ### `auth.discovery_failed` @@ -798,13 +496,17 @@ usable. In order of likelihood: - **The issuer is not exact.** It must equal the `issuer` value in the deployment's own discovery document, character for character. Fetch - `/.well-known/openid-configuration` and compare. -- **TLS is not trusted.** Common against a local Identity Server with a - self-signed certificate. Add the deployment's certificate to the operating - system trust store. The shell deliberately has no flag to skip verification. + `/.well-known/openid-configuration` and compare. The three products do + not share an issuer shape: Asgardeo's carries a `/oauth2/token` path under a + tenant, Identity Server's carries `/oauth2/token` under a host and port, and + Thunder's is the bare origin. +- **TLS is not trusted.** Common against a local Identity Server or Thunder + deployment with a self-signed certificate. Add the deployment's certificate to + the operating system trust store — each walkthrough has the commands. The + shell deliberately has no flag to skip verification. - **The machine cannot reach the issuer.** Proxy, VPN, firewall. - **The issuer does not advertise `S256`.** Set PKCE to mandatory on the - application, as in section 2.2. + application, as your walkthrough's public-client section describes. There is a third, on a device login only: @@ -812,7 +514,7 @@ There is a third, on a device login only: The deployment does not offer the grant, so there is no point printing a code nobody could approve. Either enable the **Device Code** grant on the -application (section 5.1), or use an `oauth-browser` context. Thunder-backed +application (section 3.1), or use an `oauth-browser` context. Thunder-backed deployments have no device grant at all and cannot be made to. There is a second, differently worded `auth.discovery_failed`: @@ -838,7 +540,7 @@ it expired, or it was rotated away by a concurrent run. Run `wso2 login` again. The operating system's secure store could not be used. On a headless Linux machine this usually means no Secret Service is running; start a keyring daemon, -or use a `client-credentials` context (section 7), which needs no secure store +or use a `client-credentials` context (section 5), which needs no secure store at all. ### `auth.narrowing_unavailable` @@ -854,15 +556,15 @@ The message tells you which of five things happened: | The message says | What it means | What to change | | --- | --- | --- | -| "in a form the shell cannot check" | The access token is opaque. | Set the application to issue JWT access tokens (section 2.5). | +| "in a form the shell cannot check" | The access token is opaque. | Set the application to issue JWT access tokens. | | "did not state which permissions it issued" | The deployment returned no scope, and the token claims none. | Check the API resource is authorized on the application with the scopes selected. | | "asked for the permissions X and the deployment issued Y" | The deployment ignored the narrower request and issued something else. | The deployment does not narrow on this grant. See below. | -| "is not bound to the ... audience" | The token's `aud` does not carry your audience. | The `audience` in your context document names something the deployment never puts in `aud`. Which value that is differs by product: the **client ID** on Asgardeo (section 2.5), the **API resource identifier** on Identity Server, and there only once it is in the application's audience list (section 3.5). Failing that, the resource is not authorized on the application. | +| "is not bound to the ... audience" | The token's `aud` does not carry your audience. | The `audience` in your context document names something the deployment never puts in `aud`. Which value that is differs by product: the **client ID** on [Asgardeo](login-asgardeo.md#1-what-is-different-about-asgardeo), the **API resource identifier** on [Identity Server](login-identity-server.md#1-what-is-different-about-identity-server) and there only once it is in the application's audience list, and the **resource server URI** on [Thunder](login-thunder.md#1-what-is-different-about-thunder). Failing that, the resource is not authorized on the application. | | "refused to narrow this session" | The token endpoint answered `invalid_scope`. | A scope in your context document is not one the application is authorized for. | The middle case — a deployment that will not narrow — is a property of the -deployment, not something to work around in the shell. Both supported products -do narrow: measured on 2026-08-06 against a live Asgardeo tenant and against +deployment, not something to work around in the shell. Both products this was +measured on do narrow: on 2026-08-06, against a live Asgardeo tenant and against Identity Server 7.3.0, a session carrying two permissions was refreshed down to one and answered with exactly that one. Both verdicts are in [the research document](../research/asgardeo-redirect-uri-and-scope-narrowing.md). @@ -937,7 +639,7 @@ curl -s "$(curl -s /.well-known/openid-configuration | python3 -c 'impor A serial printed as, for example, `serial=-3A4F8369` is that defect. It no longer stops a login. -On a device login (section 5.1), the message says which of four endings it was: +On a device login (section 3.1), the message says which of four endings it was: | The message says | What it means | What to do | | --- | --- | --- | @@ -952,7 +654,7 @@ code. Only the sentence differs, because only the sentence can. ### `auth.login_not_required` You ran `wso2 login` on a context whose identity carries its own credential. -There is no session to establish; just run the command (section 7). +There is no session to establish; just run the command (section 5). ### `auth.non_interactive` @@ -973,7 +675,7 @@ now names. You changed the `issuer` after logging in. Run `wso2 login` again. --- -## 9. Proving it against a real deployment +## 7. Proving it against a real deployment This repository ships a live smoke run and two one-time experiments, both behind the `smoke` build tag so they never execute in the default test gate. Neither @@ -981,7 +683,7 @@ touches your own `~/.wso2`: they write a context document into a temporary state root and store their session under the secure-store reference `wso2-cli-smoke`, deleted before and after every run. -### 9.1 First, the runs that need no deployment +### 7.1 First, the runs that need no deployment Nothing below is worth a browser sign-in until these pass. The deterministic suite already drives the whole chain — login, session, brokered acquisition — @@ -1002,7 +704,7 @@ go test -tags smoke ./test/smoke -run TestLoginSmoke -v # --- SKIP: TestLoginSmoke — no live deployment is configured: set WSO2_SMOKE_ISSUER, ... ``` -### 9.2 Describe the deployment +### 7.2 Describe the deployment Put it in a file rather than in your shell. You will end up with more than one deployment, and the variable that differs between them is not the one you would @@ -1015,7 +717,7 @@ cp test/smoke/env.example test/smoke/.env ```sh export WSO2_SMOKE_ISSUER='https://api.asgardeo.io/t//oauth2/token' export WSO2_SMOKE_CLIENT_ID='' -export WSO2_SMOKE_AUDIENCE='' # on Asgardeo — see section 2.5 +export WSO2_SMOKE_AUDIENCE='' # on Asgardeo — see its walkthrough export WSO2_SMOKE_SCOPE='reference:status:read reference:status:write' ``` @@ -1030,11 +732,15 @@ just edited. `*.env` is ignored by git. `WSO2_SMOKE_CLIENT_ID` and `WSO2_SMOKE_AUDIENCE` are different fields that Asgardeo happens to force to the same value: the first says who is asking, the -second says what the issued token must be bound to. **On Identity Server 7.x -they differ**, and the second is the resource identifier from section 2.4 — -measured on 7.3.0, section 3.5. Copying one deployment's file to another and -changing only the issuer is therefore the mistake to expect; it costs a browser -sign-in and ends in `auth.narrowing_unavailable` naming the audience. +second says what the issued token must be bound to. **On Identity Server and +Thunder they differ**: it is the API resource identifier on Identity Server and +an absolute resource-server URI on Thunder, which refuses a bare identifier — +see +[the Identity Server walkthrough](login-identity-server.md#1-what-is-different-about-identity-server) +and [the Thunder walkthrough](login-thunder.md#1-what-is-different-about-thunder). +Copying one deployment's file to another and changing only the issuer is +therefore the mistake to expect; it costs a browser sign-in and ends in +`auth.narrowing_unavailable` naming the audience. Confirm the issuer against the deployment's own document before spending a sign-in on a value that is close but not exact: @@ -1048,11 +754,11 @@ The printed issuer must equal `WSO2_SMOKE_ISSUER` character for character, and `S256` must appear. Those are the two most common reasons a first login fails before it reaches a browser. -### 9.3 The live runs +### 7.3 The live runs ```sh make smoke-login # log in, prove the session persisted, broker one acquisition -make smoke-login-device # the same, approved on another device (section 5.1) +make smoke-login-device # the same, approved on another device (section 3.1) make empirical-asgardeo # answer the two open questions about Asgardeo's behavior ``` @@ -1071,7 +777,7 @@ LOGIN SMOKE: granted — access of 1219 characters bound to "", expiri A run that ends in `auth.narrowing_unavailable` **also passes**, and that is deliberate: the shell refusing to hand a module more authority than it asked for -is the designed outcome, not a fallback. Section 8 decodes which of the five +is the designed outcome, not a fallback. Section 6 decodes which of the five narrowing refusals you got. The experiments print one verdict line each. Their answers belong in section 3 diff --git a/docs/research/asgardeo-redirect-uri-and-scope-narrowing.md b/docs/research/asgardeo-redirect-uri-and-scope-narrowing.md index 9981130..6b729f9 100644 --- a/docs/research/asgardeo-redirect-uri-and-scope-narrowing.md +++ b/docs/research/asgardeo-redirect-uri-and-scope-narrowing.md @@ -280,7 +280,7 @@ for one namespace from one brokered for another. ### 3.1 The same questions against Identity Server 7.3.0 Measured 2026-08-06 against `https://localhost:9443/oauth2/token` — the -`wso2/wso2is:7.3.0` container, registered as the walkthrough's section 3 +`wso2/wso2is:7.3.0` container, registered as the Identity Server walkthrough describes. This is a second deployment, not a second reading of the first: the cells above stay Asgardeo's. diff --git a/docs/research/product-authentication-compatibility.md b/docs/research/product-authentication-compatibility.md index 2b37bb3..b51fdca 100644 --- a/docs/research/product-authentication-compatibility.md +++ b/docs/research/product-authentication-compatibility.md @@ -55,7 +55,10 @@ wso2-cli needs per-context client configuration or registration. (`wso2/identity-apps`), and `wso2/product-is` for Identity Server.** The wso2-cli login slice ships *against* this gap rather than waiting for it to close: every tenant and every deployment registers its own public client by -hand, and [the login walkthrough](../guides/login.md) §§2–3 is that manual +hand, and the per-product login walkthroughs — +[Asgardeo](../guides/login-asgardeo.md), +[Identity Server](../guides/login-identity-server.md), +[ThunderID](../guides/login-thunder.md) — are that manual registration written out in full — a standard-based application, public client, PKCE mandatory with `S256`, the four loopback callbacks `http://127.0.0.1:{10425,10426,10427,10428}/callback`, the refresh-token grant, diff --git a/test/smoke/RUNNING.md b/test/smoke/RUNNING.md index 9de4a1c..8138f99 100644 --- a/test/smoke/RUNNING.md +++ b/test/smoke/RUNNING.md @@ -1,7 +1,10 @@ # Running the live smoke and the empirical experiments **Status:** Working draft -**Related:** [Login walkthrough](../../docs/guides/login.md), +**Related:** [Login guide](../../docs/guides/login.md) and its per-product +walkthroughs — [Asgardeo](../../docs/guides/login-asgardeo.md), +[Identity Server](../../docs/guides/login-identity-server.md), +[ThunderID](../../docs/guides/login-thunder.md), [Asgardeo redirect URIs and scope narrowing](../../docs/research/asgardeo-redirect-uri-and-scope-narrowing.md) Everything in this directory except `config.go` is behind the `smoke` build tag. @@ -13,8 +16,11 @@ These runs need a human. They open a real browser and wait for a real sign-in. ## What to export -Register the application first — [the walkthrough](../../docs/guides/login.md) -covers Asgardeo and Identity Server 7.x — then describe it with these variables. +Register the application first — there is a walkthrough per product: +[Asgardeo](../../docs/guides/login-asgardeo.md), +[Identity Server 7.x](../../docs/guides/login-identity-server.md), +[ThunderID](../../docs/guides/login-thunder.md) — then describe it with these +variables. Describing it once in a file beats re-exporting it into every shell. Copy [`env.example`](env.example) and fill it in: @@ -145,12 +151,16 @@ export WSO2_SMOKE_IDENTITY_TYPE=onprem The other is the audience. On Asgardeo it has to be the **client ID**, because that is the only value Asgardeo ever puts in an access token's `aud`. On Identity Server it is the **API resource identifier**, which reaches `aud` once -the identifier is in the application's audience list. Sections 2.5 and 3.5 of -[the walkthrough](../../docs/guides/login.md) cover both. This is the main -reason to keep a file per deployment rather than editing one in place. +the identifier is in the application's audience list. Section 1 of +[each](../../docs/guides/login-asgardeo.md) +[product's](../../docs/guides/login-identity-server.md) +[walkthrough](../../docs/guides/login-thunder.md) states its own answer and the +measurement behind it. This is the main reason to keep a file per deployment +rather than editing one in place. -A local Identity Server also has to be trusted by the operating system before -any of this can reach it — see section 3.6 of the walkthrough. +A local Identity Server or Thunder deployment also has to be trusted by the +operating system before any of this can reach it — see section 3 of its +walkthrough. ### The one refusal that is not a failure @@ -186,7 +196,7 @@ one-permission request with both. The other four read: | did not state which permissions it issued | neither the response nor the token named a scope | | not bound to the *audience* | the token is real but carries a different `aud` — on Asgardeo, almost always because the audience is set to the API resource rather than the client ID | -Section 8 of [the walkthrough](../../docs/guides/login.md) tabulates the same +Section 6 of [the login guide](../../docs/guides/login.md) tabulates the same five against what to change in the registration. Read which acquisition refused, too. On the **narrowed** one it is a statement @@ -282,16 +292,16 @@ only verdicts whose deployment line names the deployment you mean to record. - `inconclusive (unrecognized narrowing refusal)` — the deployment refused and the refusal did not name permissions, so which of the narrowing causes it was cannot be read off it. Record nothing from this one. The run's own output - carries the underlying message; section 8 of - [the walkthrough](../../docs/guides/login.md) maps it to what to change. + carries the underlying message; section 6 of + [the login guide](../../docs/guides/login.md) maps it to what to change. - `inconclusive (audience not bound)` — a token came back that is not bound to the configured audience. On Asgardeo this is not a registration defect to fix: Asgardeo binds a JWT access token's `aud` claim to the **client ID**, never to the API resource identifier whose scopes the token carries, and this is not configurable — the application's Protocol tab exposes an Audience field only under **ID Token**, and the Access Token section has no audience - control at all. See section 2.5 of - [the walkthrough](../../docs/guides/login.md), and + control at all. See section 1 of + [the Asgardeo walkthrough](../../docs/guides/login-asgardeo.md), and [the research document](../../docs/research/asgardeo-redirect-uri-and-scope-narrowing.md) this file already links above. The remedy is to set `WSO2_SMOKE_AUDIENCE` here, and `products..audience` in a real context document, to the diff --git a/test/smoke/config.go b/test/smoke/config.go index 4ad0e9d..1bf53e9 100644 --- a/test/smoke/config.go +++ b/test/smoke/config.go @@ -25,7 +25,7 @@ // the two ways a live run wastes a human's attention, and both are decided // before any browser opens. // -// See test/smoke/RUNNING.md for what to export and docs/guides/login.md for how +// See test/smoke/RUNNING.md for what to export and docs/guides/login-*.md for how // to register the application the variables describe. package smoke diff --git a/test/smoke/env.example b/test/smoke/env.example index e62820f..5eb00f6 100644 --- a/test/smoke/env.example +++ b/test/smoke/env.example @@ -29,7 +29,9 @@ # disk and defeat the point. # # See RUNNING.md in this directory for what every variable means, and -# docs/guides/login.md for how to register the application they describe. +# docs/guides/login.md for the shared path, and its per-product walkthroughs — +# login-asgardeo.md, login-identity-server.md, login-thunder.md — for how to +# register the application they describe. # --- Asgardeo ------------------------------------------------------------ @@ -46,7 +48,7 @@ export WSO2_SMOKE_SCOPE='reference:status:read reference:status:write' # costs a browser sign-in and ends in auth.narrowing_unavailable. # # Written as a reference rather than as the same string twice, so the two cannot -# drift apart. Section 2.5 of the walkthrough explains what this costs: the +# drift apart. Section 1 of login-asgardeo.md explains what this costs: the # broker's audience check still proves a token was minted for this client, but # on Asgardeo it cannot tell one product's tokens from another's. export WSO2_SMOKE_AUDIENCE="$WSO2_SMOKE_CLIENT_ID" @@ -56,7 +58,8 @@ export WSO2_SMOKE_AUDIENCE="$WSO2_SMOKE_CLIENT_ID" # Delete the block above and uncomment this one. Here AUDIENCE is the API # resource identifier: Identity Server puts every entry of the application's # audience list into the access token beside the client ID, so the broker can -# tell one product's tokens from another's. Section 3.5 records the difference. +# tell one product's tokens from another's. Section 1 of +# login-identity-server.md records the difference. # # Confirm the issuer rather than assuming it. A deployment with a port offset # answers on 9444 or higher, and the shell checks the discovery document against @@ -66,7 +69,7 @@ export WSO2_SMOKE_AUDIENCE="$WSO2_SMOKE_CLIENT_ID" # curl -sk https://localhost:9443/oauth2/token/.well-known/openid-configuration # # This deployment also has to be trusted by the operating system before login -# can reach it at all. See section 3.6 of the walkthrough. +# can reach it at all. See section 3 of login-identity-server.md. # export WSO2_SMOKE_ISSUER='https://localhost:9443/oauth2/token' # export WSO2_SMOKE_CLIENT_ID='' diff --git a/test/smoke/login_smoke_test.go b/test/smoke/login_smoke_test.go index 11519c7..eef607d 100644 --- a/test/smoke/login_smoke_test.go +++ b/test/smoke/login_smoke_test.go @@ -188,7 +188,7 @@ func acquire(t *testing.T, broker *auth.Broker, request auth.Request, verdict, a // // auth.narrowing_unavailable covers five distinct causes — see // internal/auth/narrowing.go's verify() and the table under - // auth.narrowing_unavailable in docs/guides/login.md section 8 — so this + // auth.narrowing_unavailable in docs/guides/login.md section 6 — so this // summary must not name one of them (a "narrowed grant" specifically). // The interpolated error text below is what actually says which of the // five happened; this sentence only states what is true regardless: the