Single sign-on lets an application rely on an identity provider’s authentication result. The user does not need to hand that provider password to each application. The application still owns its authorization decisions, local sessions, and integration configuration; confidential clients may also hold client credentials. Delegating authentication does not remove the application’s security boundary or guarantee that every future login can skip user interaction.

An OIDC provider relays an ID Token into the application’s separate verification and request-binding boundary. App session and authorization decisions remain local. Separate lifetimes require a logout contract. The article’s fabricated user-7 and admin strings both decode, but verified remains False; actual negative integration tests are still required.
SSO relays an authentication result into the application’s own security boundary. The app still verifies the token, binds the request, and controls sessions and access. Decoding establishes no identity; ID Token expiry does not automatically end an application session.

Identify the protocol and the token

SAML and OpenID Connect support federation through different protocols, not merely interchangeable XML and JSON encodings. This article focuses on OIDC: an identity layer over OAuth 2.0 that returns an ID Token about authentication. An access token authorizes access to a resource and may use a different format. A JWT-shaped string is not automatically an ID Token, and a resource server should not accept one just because it came from a familiar provider.

For an OIDC ID Token, validate the expected issuer, intended client audience, expiration, and any requested nonce using the selected flow’s rules. Apply the relevant authorized-party and authentication-context checks too. A correctly signed token for another client is still unsuitable. Match the signing keys to the trusted issuer; an attacker-controlled issuer string must not select an arbitrary metadata server.

Configure the verifier’s allowed algorithms and key types independently of untrusted input. The header’s algorithm must agree with that policy and the operation actually performed. HS256 is not inherently invalid: OIDC defines negotiated MAC-based signing with an appropriate shared secret. Treating an RSA public key as that secret is algorithm confusion. An unfamiliar key identifier may be legitimate rotation, but must never justify skipping verification.

When it breaks

Decoding is the most misleading success. Base64url and JSON parsing can reveal plausible claims without establishing who issued them. An administrative subject, matching audience, and future expiration can all be invented. Do not paste live tokens into online decoders or incident logs; they can expose personal claims and usable credentials. Inspect only selected fields through an approved diagnostic path.

Login-flow binding matters alongside token validation. Use a maintained OIDC integration with the redirect, request-correlation, and PKCE protections appropriate to its client and flow. RFC 9700 gives current OAuth security guidance. A checklist of four JWT fields cannot replace that integration, and accepting a token from the wrong request can remain dangerous even when its signature is genuine.

Check it quickly

Save this as inspect-sso-fixture.py and run python3 inspect-sso-fixture.py with assertions enabled. It uses only fabricated values, no provider, keys, credentials, network, or third-party packages. This is a complete demonstration of decoding’s limits, not a production token parser or authentication implementation:

import base64
import json


def encode(value):
    raw = json.dumps(value, separators=(",", ":")).encode()
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()


def inspect_fixture(token):
    header, payload, signature = token.split(".")
    decode = lambda part: json.loads(base64.urlsafe_b64decode(
        part + "=" * (-len(part) % 4)))
    return {"header": decode(header), "claims": decode(payload),
            "signature_text": signature, "verified": False}


header = {"alg": "RS256", "kid": "invented-key"}
claims = {"iss": "https://issuer.example", "aud": "billing",
          "sub": "user-7", "exp": 2000000000}
first = encode(header) + "." + encode(claims) + ".not-a-signature"
second = encode(header) + "." + encode(dict(claims, sub="admin")) + ".not-a-signature"
rows = [inspect_fixture(token) for token in (first, second)]
assert [row["claims"]["sub"] for row in rows] == ["user-7", "admin"]
assert all(row["header"]["alg"] == "RS256" for row in rows)
assert all(row["verified"] is False for row in rows)
print("decoded subjects: user-7, admin")
print("cryptographic verification performed: False")

Both strings name RS256, billing, and the same issuer. Changing the subject to admin requires no signing key because the final component is invented text. The script prints both subjects and explicitly reports that no cryptographic verification occurred. It does not validate signatures, expiry, audience, or issuer discovery; the false verification field prevents the output from masquerading as an authentication result.

For a real integration, a separate test suite must reject a wrong issuer, wrong audience, expired token, failed signature, mismatched request nonce, and inappropriate token type. Exercise supported key rotation and error handling with the actual library and provider. Those integration cases are requirements to verify in that environment, not results established by this local fixture.

Give logout its own contract

The provider session, application session, access token, and refresh token have separate lifetimes. Expiration of the ID Token used during login does not automatically destroy an application cookie or server-side session. Short token lifetimes therefore do not, by themselves, define how quickly disabling an account removes existing application access.

JWT’s jti is an optional identifier, not a mandatory universal revocation-list lookup. If immediate logout is required, choose a supported mechanism and verify the application actually ends the identified sessions. OIDC back-channel logout defines a separately validated logout message and session-clearing action. Token introspection, where supported, answers a token-status question; it does not automatically invalidate an unrelated application session. Record the offboarding guarantee and its failure behavior explicitly.

Sources