---
title: "The Login Page Was Public. The Identity Boundary Was Somewhere Else."
description: "A public login page is only the visible start of an identity system. External pentesting must map the issuer, callback, token policy, account binding, tenant and role conversion, and local session before testing authorization."
date: 2026-09-11
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/login-identity-boundary/
---

## Identity boundary mapping in 60 seconds

A public application redirects the browser to an identity provider. The user signs in, returns to
`/callback`, and receives an application cookie. It is tempting to record “OIDC login” and move on.
But the visible login page is only the first frame of a longer authority decision.

The application must still decide:

- which issuer and client this response belongs to;
- whether the callback belongs to the flow that this browser started;
- which token type, signature algorithm, key, audience, nonce, and lifetime are acceptable;
- which external identity becomes which local account;
- which organisation, tenant, role, and permissions that account receives;
- when a new local session is created, rotated, refreshed, revoked, or recovered;
- whether logout, password reset, account disablement, and role changes alter existing sessions.

That sequence—not the login form—is the identity boundary. An identity provider can authenticate a
person correctly while the customer application binds that identity to the wrong tenant, accepts a
token intended for another client, preserves a pre-authentication session, or converts an ordinary
claim into administrative authority.

The external pentester's task is therefore not to attack the provider. It is to reconstruct the
customer-controlled identity contract, then use approved test identities to verify the decisions
that create local authority.

## Authentication is not the same decision as authorization

Authentication answers a narrow question: which identity did a trusted authenticator establish?
The business application has a different problem: what may that identity do here, in this tenant,
with this object, at this point in the workflow?

OpenID Connect makes the distinction visible. An ID Token carries claims about an authentication
event and an end user. The relying party still has to validate the token and decide how its claims
map into local state. In particular, the [OpenID Connect Core
specification](https://openid.net/specs/openid-connect-core-1_0-18.html) defines `iss`, `sub`, and
`aud` as separate identity coordinates. A subject value is locally unique within an issuer; the
same `sub` under a different issuer is not the same identity. The audience identifies the client
for which the token was issued. A valid signature without those policy checks is not sufficient.

The JWT security best-practice document makes the application responsibility explicit: keys must
belong to the trusted issuer, the issuer-subject pair must be valid for the application, and an
audience intended for the recipient must be present and checked where tokens can serve multiple
applications. See [RFC 8725](https://www.rfc-editor.org/rfc/rfc8725.html).

This produces a useful model:

> The provider establishes an external identity. The application decides whether that identity is
> acceptable, which local principal it becomes, and which authority that principal receives.

When those three steps are collapsed into “SSO works,” the highest-value customer-controlled
decisions disappear from the test plan.

## The visible flow hides seven decision points

<figure class="diagram">
<svg viewBox="0 0 720 322" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A public login start crosses authorization, callback, token validation, account binding, tenant and role mapping, and local session boundaries before reaching an application action.">
<defs>
<marker id="lib-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker>
</defs>
<rect x="12" y="22" width="132" height="48" rx="8" class="dg-box-accent" />
<text x="26" y="42" class="dg-label">public entry</text><text x="26" y="58" class="dg-accent">LOGIN START</text>
<line x1="146" y1="46" x2="176" y2="46" class="dg-line" marker-end="url(#lib-a)" />
<rect x="180" y="22" width="146" height="48" rx="8" class="dg-box" />
<text x="194" y="42" class="dg-label">authorization server</text><text x="194" y="58" class="dg-muted">ISSUER · CLIENT</text>
<line x1="328" y1="46" x2="358" y2="46" class="dg-line" marker-end="url(#lib-a)" />
<rect x="362" y="22" width="160" height="48" rx="8" class="dg-box" />
<text x="376" y="42" class="dg-label">browser return</text><text x="376" y="58" class="dg-muted">CALLBACK · STATE</text>
<line x1="524" y1="46" x2="554" y2="46" class="dg-line" marker-end="url(#lib-a)" />
<rect x="558" y="22" width="150" height="48" rx="8" class="dg-box-accent" />
<text x="572" y="42" class="dg-label">token policy</text><text x="572" y="58" class="dg-accent">SIG · AUD · NONCE</text>
<line x1="633" y1="72" x2="633" y2="112" class="dg-line" marker-end="url(#lib-a)" />
<rect x="558" y="116" width="150" height="48" rx="8" class="dg-box" />
<text x="572" y="136" class="dg-label">account binding</text><text x="572" y="152" class="dg-muted">ISS + SUB → USER</text>
<line x1="556" y1="140" x2="526" y2="140" class="dg-line" marker-end="url(#lib-a)" />
<rect x="350" y="116" width="172" height="48" rx="8" class="dg-box" />
<text x="364" y="136" class="dg-label">authority conversion</text><text x="364" y="152" class="dg-muted">TENANT · ROLE · SCOPE</text>
<line x1="348" y1="140" x2="318" y2="140" class="dg-line" marker-end="url(#lib-a)" />
<rect x="146" y="116" width="168" height="48" rx="8" class="dg-box-accent" />
<text x="160" y="136" class="dg-label">local session</text><text x="160" y="152" class="dg-accent">ROTATE · EXPIRE</text>
<line x1="230" y1="166" x2="230" y2="206" class="dg-line" marker-end="url(#lib-a)" />
<rect x="146" y="210" width="168" height="48" rx="8" class="dg-box" />
<text x="160" y="230" class="dg-label">application action</text><text x="160" y="246" class="dg-muted">OBJECT · EFFECT</text>
<line x1="316" y1="234" x2="352" y2="234" class="dg-line" marker-end="url(#lib-a)" />
<rect x="356" y="210" width="166" height="48" rx="8" class="dg-box-accent" />
<text x="370" y="230" class="dg-label">negative control</text><text x="370" y="246" class="dg-accent">MUST BE DENIED</text>
<text x="12" y="296" class="dg-crit">REAL BOUNDARY</text><text x="140" y="296" class="dg-mono">external identity becomes local account, tenant, role and session authority</text>
</svg>
<figcaption>The login page starts the journey. Customer-controlled authority is created only after callback correlation, token policy, local account binding, role and tenant conversion, and session creation.</figcaption>
</figure>

The seven points require different evidence.

| Decision point            | Evidence to preserve                                             | Security question                                                         |
| ------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Login start               | Application route, parameters, cookies, response, timestamp      | Did the application initiate one specific flow for this browser?          |
| Authorization request     | Issuer host, client ID, redirect URI, scopes, state, nonce, PKCE | Is the request bound to the correct provider, client, and return route?   |
| Callback                  | Return path, response mode, code or assertion, correlation state | Can a response be substituted, replayed, or attached to another flow?     |
| Token validation          | Token type, header, claims, key source, validation result        | Are signature, issuer, audience, nonce, time, and token purpose enforced? |
| Account binding           | Issuer-subject pair and local account identifier                 | Can two external identities collide or link to the same local account?    |
| Authority conversion      | Tenant, organisation, role, group, scope, entitlement source     | Which claim or local record grants authority, and which one wins?         |
| Local session and actions | Cookie lifecycle, account context, API calls, object effects     | Does authority stay correct across login, refresh, change, and logout?    |

## Map the identity contract before testing it

The first pass should be observational. Start from the approved application entry point and record
the redirect chain without attempting to enumerate provider tenants or probe real user accounts.
Public metadata, customer configuration, browser requests, response headers, and the supplied test
accounts are usually enough to establish the contract.

For each application, record:

1. **Entry point:** the customer URL that starts login, including regional, partner, admin, mobile,
   and recovery variants.
2. **Protocol role:** relying party or client, authorization server or identity provider, resource
   server, and any broker between them.
3. **Issuer and endpoints:** exact issuer identifier, authorization and token endpoints, key source,
   callback routes, logout routes, and recovery routes.
4. **Client context:** client ID, redirect URI, response type, response mode, scopes, and whether
   PKCE, `state`, and `nonce` appear.
5. **Identity key:** the fields used to bind the provider identity to a local account. Preferably
   this is the stable issuer-subject pair—not an editable email address or display name.
6. **Authority source:** local database, provider group, application role, tenant membership,
   invitation, support override, or a combination.
7. **Session model:** pre-login cookie, post-login cookie, refresh behavior, idle and absolute
   lifetime, concurrent sessions, logout, revocation, and device or tenant switching.
8. **Lifecycle edges:** first login, invitation acceptance, account linking, email change, role
   change, tenant removal, password reset, MFA recovery, disablement, and deletion.

This inventory turns “uses SSO” into a falsifiable system model. Unknown fields stay marked unknown;
they are not silently filled with provider defaults.

## Protocol correctness is necessary, but policy creates the boundary

[RFC 9700](https://www.rfc-editor.org/rfc/rfc9700.html), the OAuth 2.0 Security Best Current
Practice, recommends exact matching of registered redirect URIs, authorization code flows rather
than exposing access tokens in browser URLs, and protections such as PKCE against authorization
code injection. Those controls matter, but a protocol-compliant exchange can still lead to an
unsafe local decision.

Consider four examples:

**A trusted issuer, wrong audience.** The signature is genuine and the token is current, but it was
created for another client in the same identity estate. If the application verifies only the
signature, it may accept authority never issued to it.

**A correct user, wrong tenant.** The issuer and subject are valid, but an organisation identifier
is taken from a browser parameter after login rather than from verified membership. Authentication
succeeds; tenant authorization fails.

**A correct claim, wrong meaning.** A provider group called `billing-admin` may be informational in
one application and privileged in another. The danger is not the claim's authenticity. It is an
undocumented conversion from a broad directory group into a local administrative role.

**A correct login, stale local session.** The account is disabled or removed from a tenant at the
provider, but an application session continues with cached authority. The authentication event was
correct when it occurred; the lifecycle contract failed afterward.

The test plan should therefore distinguish two layers:

- **protocol acceptance:** can this response, token, or assertion be accepted by this client?
- **business authority:** after acceptance, which local account, tenant, role, objects, and effects
  become reachable?

Testing only the first layer produces a token checklist. Testing both layers produces an identity
assessment.

## The session is where external identity becomes local persistence

Federated login does not remove application session security. It moves the authentication event to
another system, then asks the customer application to create and maintain local state.

Capture the unauthenticated cookie before login and compare it with the authenticated cookie after
the callback. A privileged transition should not simply bless attacker-known pre-authentication
state. OWASP's [session fixation testing
guidance](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/03-Testing_for_Session_Fixation)
frames the core question: does the application issue a fresh session after successful
authentication?

Rotation at login is only the first control. The same analysis belongs at every authority change:

| Transition                | Expected local behavior                                                        |
| ------------------------- | ------------------------------------------------------------------------------ |
| Anonymous → authenticated | Replace or safely rotate the session; bind it to the authenticated principal   |
| User → administrator      | Re-evaluate authority and rotate sensitive session state where appropriate     |
| Tenant A → tenant B       | Change tenant context explicitly; prevent object and cache bleed               |
| Refresh                   | Preserve correct issuer, client, subject, tenant, and scope constraints        |
| Password or MFA recovery  | Invalidate or deliberately govern older sessions and recovery artifacts        |
| Role or group removal     | Remove effective authority within a defined, testable interval                 |
| Account disablement       | Deny new sessions and revoke or expire existing sessions according to policy   |
| Logout                    | Terminate the local session; distinguish local logout from provider SSO logout |

Cookie flags are useful evidence, not a complete conclusion. `Secure`, `HttpOnly`, `SameSite`,
`Domain`, `Path`, and lifetime settings describe how the browser handles an artifact. They do not
prove that the server rotates it, revokes it, binds it to the correct tenant, or stops accepting it
after the account changes.

## Build a role and tenant matrix from controlled identities

Identity testing becomes defensible when every result has a comparison. Ask the customer for the
smallest synthetic identity set that represents the important boundaries: unauthenticated, ordinary
user, a second user in the same tenant, a user in another tenant, and one approved privileged role.
Use customer-owned data and reversible actions.

The [OWASP role-definition testing
guidance](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/03-Identity_Management_Testing/01-Test_Role_Definitions)
starts from identifying and documenting the application's roles. For a real assessment, extend that
role list into a matrix of identity, tenant, object ownership, workflow state, and expected effect.

| Test identity | Tenant context | Object relation | Function               | Expected result                 |
| ------------- | -------------- | --------------- | ---------------------- | ------------------------------- |
| Anonymous     | None           | None            | View account dashboard | Deny and start login            |
| User A        | Meridian       | Owner           | Read own invoice       | Allow                           |
| User B        | Meridian       | Same tenant     | Read User A invoice    | Deny unless explicitly shared   |
| User C        | Northwind      | Other tenant    | Read User A invoice    | Deny                            |
| Billing admin | Meridian       | Tenant managed  | Approve invoice        | Allow with recorded audit event |
| User A        | Meridian       | Owner           | Assign admin role      | Deny                            |

The most important column is the expected result. Without it, an HTTP `200` is merely an
observation. With it, the same response can prove either intended access or an authorization
failure. Record the downstream effect as well: data returned, record changed, message queued,
privilege granted, or nothing happened.

## Negative controls make the finding believable

An identity result needs more than one successful request. A strong proof contains a valid control,
an invalid comparison, and an observable effect.

<figure class="diagram">
<svg viewBox="0 0 720 304" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="An identity hypothesis is tested with an allowed control, denied cross-user and cross-tenant comparisons, and a verified downstream effect before becoming a finding.">
<defs>
<marker id="lib-b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker>
</defs>
<rect x="14" y="24" width="168" height="54" rx="8" class="dg-box-accent" />
<text x="28" y="45" class="dg-label">identity hypothesis</text><text x="28" y="63" class="dg-accent">CLAIM → AUTHORITY?</text>
<line x1="184" y1="51" x2="218" y2="51" class="dg-line" marker-end="url(#lib-b)" />
<rect x="222" y="24" width="148" height="54" rx="8" class="dg-box" />
<text x="236" y="45" class="dg-label">positive control</text><text x="236" y="63" class="dg-muted">OWNER · ALLOW</text>
<line x1="372" y1="51" x2="406" y2="51" class="dg-line" marker-end="url(#lib-b)" />
<rect x="410" y="24" width="142" height="54" rx="8" class="dg-box" />
<text x="424" y="45" class="dg-label">same tenant</text><text x="424" y="63" class="dg-muted">NON-OWNER · DENY</text>
<line x1="554" y1="51" x2="588" y2="51" class="dg-line" marker-end="url(#lib-b)" />
<rect x="592" y="24" width="114" height="54" rx="8" class="dg-box" />
<text x="606" y="45" class="dg-label">other tenant</text><text x="606" y="63" class="dg-muted">DENY</text>
<line x1="649" y1="80" x2="649" y2="120" class="dg-line" marker-end="url(#lib-b)" />
<rect x="532" y="124" width="174" height="54" rx="8" class="dg-box-accent" />
<text x="546" y="145" class="dg-label">effect verification</text><text x="546" y="163" class="dg-accent">READ · CHANGE · EVENT</text>
<line x1="530" y1="151" x2="496" y2="151" class="dg-line" marker-end="url(#lib-b)" />
<rect x="320" y="124" width="172" height="54" rx="8" class="dg-box" />
<text x="334" y="145" class="dg-label">repeat and bound</text><text x="334" y="163" class="dg-muted">ONE VARIABLE</text>
<line x1="318" y1="151" x2="284" y2="151" class="dg-line" marker-end="url(#lib-b)" />
<rect x="110" y="124" width="170" height="54" rx="8" class="dg-box-accent" />
<text x="124" y="145" class="dg-label">defensible result</text><text x="124" y="163" class="dg-accent">EVIDENCE · IMPACT</text>
<text x="14" y="226" class="dg-crit">CHANGE ONE VARIABLE</text><text x="194" y="226" class="dg-mono">identity, tenant, role, object, state or session age</text>
<text x="14" y="258" class="dg-crit">STOP CONDITION</text><text x="158" y="258" class="dg-mono">unexpected real-user data, cross-tenant effect or provider impact</text>
<text x="14" y="286" class="dg-muted">A successful request without a denied comparison does not locate the authorization boundary.</text>
</svg>
<figcaption>Change one authority variable at a time. The result becomes reportable only when the expected control, denied comparison, and real application effect establish the boundary.</figcaption>
</figure>

Useful negative controls include:

- the same object with a different approved user;
- the same role in a different approved tenant;
- the same session after logout, role removal, or account disablement;
- the same callback with missing or mismatched flow correlation in a controlled test;
- the same signed token with the wrong intended audience in a customer test environment;
- the same function reached through UI and API routes;
- the same business action in an invalid workflow state.

Do not turn this matrix into indiscriminate token mutation. The purpose is to isolate the
customer's validation and authorization decisions with the minimum requests necessary, not to
flood an identity provider with malformed traffic.

## Keep the provider outside the line of fire

The identity provider may be architecturally important and contractually out of scope. That is
normal. The assessment can still evaluate the customer-controlled relying party, client
registration, callback handler, account-linking logic, local role conversion, API authorization,
and session lifecycle.

Before active testing, make the boundary explicit:

- approved customer applications, callback routes, tenants, accounts, roles, and source addresses;
- whether the customer can provide a test identity tenant or isolated federation configuration;
- permitted login volume and MFA behavior;
- prohibited actions such as password spraying, real-user discovery, unsolicited MFA prompts,
  provider-tenant enumeration, or testing provider infrastructure;
- test objects and reversible effects;
- contacts and stop conditions for lockout, cross-tenant access, production data, or unexpected
  provider behavior.

A provider hostname in a redirect is evidence of a trust relationship, not permission to test the
provider. If a hypothesis can only be proved by acting against the provider or real users, stop and
request a scope decision or a customer-controlled test environment.

## A fictional identity-boundary review

Assume the previous article identified `partners.meridian.example` as a current, approved partner
portal. The public route redirects to a corporate identity service, then returns to
`/auth/callback` and creates a local session.

1. The analyst records the initial application cookie, redirect chain, exact issuer, client ID,
   redirect URI, scopes, `state`, `nonce`, and PKCE parameters. No provider endpoints beyond the
   normal customer login flow are probed.
2. Customer documentation confirms that the portal trusts one issuer and that partner membership
   should be stored locally. Two synthetic partner tenants and one billing-admin account are
   supplied.
3. The valid flow proves that the issuer, callback, token, local account, tenant, and session can
   produce the expected User A dashboard. This is the positive control.
4. Comparing the pre-login and post-login cookies shows rotation. Replaying the pre-login cookie
   after authentication remains anonymous. Session fixation is not supported.
5. The application binds the external identity by issuer and subject, not by editable email. A
   second identity with the same synthetic email under the approved alternate test issuer does not
   collide with the first local account.
6. The UI takes a `workspace` parameter, but the API derives effective tenant membership from the
   local session. Changing the browser parameter changes presentation only; cross-tenant objects
   remain denied.
7. One problem remains: the billing-admin role is copied into the session at login and is not
   re-evaluated after customer administrators remove the role. The old session can approve a
   synthetic invoice until its eight-hour expiry. A new session is correctly denied.
8. The analyst records the positive admin control, the removed-role negative expectation, the old
   and new session comparison, the synthetic approval event, timestamps, and cleanup. Testing stops
   after one reversible object demonstrates the stale-authority window.

The finding is not “the provider issued a bad token.” The provider authenticated and described the
test identity correctly. The customer application retained local authority after the source of that
authority changed. The fix therefore belongs in session invalidation, entitlement refresh, and
revocation design—not in the provider's login screen.

## Evidence matrix

| Observation or test                                | What it supports                                             | What it cannot prove                                        | Required next step                                               |
| -------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------- |
| Public route redirects to an identity provider     | An identity relationship exists in this browser flow         | Provider ownership, tenant scope, or customer authorization | Record exact issuer, client, callback, and customer confirmation |
| OIDC metadata exposes endpoints and keys           | Published protocol configuration for an issuer               | That the application trusts or validates every field        | Compare metadata with client behavior and configuration          |
| Token signature validates                          | Token integrity under the selected key and algorithm         | Trusted issuer, intended audience, valid subject, or role   | Verify the complete application acceptance policy                |
| ID Token contains email, groups, tenant, and roles | The issuer asserted those claims                             | Which claims create local identity or authority             | Trace account binding and role conversion                        |
| Callback succeeds with a normal flow               | One positive authentication path works                       | Resistance to substitution, replay, or correlation failure  | Run bounded negative controls                                    |
| Cookie flags are present                           | Browser handling restrictions exist                          | Rotation, server revocation, tenant binding, or logout      | Compare session behavior across lifecycle transitions            |
| Different role sees different navigation           | The UI presents role-specific content                        | Server-side function or object authorization                | Compare direct approved API requests and effects                 |
| Cross-tenant request returns a generic denial      | One tested path enforced a tenant boundary                   | All object classes, alternate routes, or workflow states    | Expand by risk and object class, not by blind enumeration        |
| Removed role remains active in old session         | Local authority can outlive its source for the measured time | Universal account takeover or provider failure              | Bound duration, functions, revocation behavior, and impact       |
| Logout removes the browser cookie                  | Client-side session artifact was cleared                     | Server-side invalidation or provider-wide logout            | Reuse the old artifact safely and distinguish logout domains     |

## What defenders should receive

The deliverable should let identity, application, and platform teams see the same boundary.

**Identity contract.** Issuer, client, callback, key source, subject key, accepted token types,
audience, correlation controls, and lifecycle assumptions.

**Authority conversion map.** External claims and local records that determine account, tenant,
role, scope, and exceptional access. Conflicts and precedence rules should be explicit.

**Session lifecycle.** Creation, rotation, refresh, expiry, logout, revocation, role change,
tenant removal, disablement, and recovery behavior, with measured windows rather than vague labels.

**Permission matrix.** Synthetic identities, roles, tenants, objects, workflow states, expected
results, observed results, and evidence references.

**Rejected hypotheses.** Controls that worked should remain in the record: audience mismatch was
rejected, pre-login state was rotated, cross-tenant reads failed, account identities did not
collide. They define the tested boundary and prevent future teams from repeating the same work.

**Actionable finding.** The smallest failing policy, its application owner, reproducible evidence,
bounded effect, cleanup, and a retest that includes both positive and negative controls.

## The series ends where active testing can finally begin

This series started with a simple claim: the external perimeter is a graph, and a port list is only
one view. Each article then removed one dangerous shortcut.

1. A company name became an entity graph, not an automatic target list.
2. A domain relationship became attributed evidence, not assumed ownership or scope.
3. A third-party platform became a customer-controlled trust boundary, not permission to test the
   vendor.
4. A passive service observation became a current application model, not a vulnerability claim.
5. A public login page became an identity, tenant, role, and session contract, not proof that
   authorization was understood.

Only now is the test team ready to move from intelligence into application hypotheses. It knows
which organisation owns the surface, which names and services are current, which suppliers carry
authority, which application workflows matter, which identities and tenants are approved, and what
evidence will distinguish intended access from a security failure.

That is the real value of quieter external pentesting. Passive intelligence does not replace active
testing. It ensures that each active request reaches a boundary the customer actually owns, asks a
question the application can answer, and produces evidence a defender can use.