---
title: 'Enterprise Federation'
source: 'https://academia.sh/en/courses/authentication-and-authorization/enterprise-federation'
course: 'Authentication and Authorization'
language: en
updated: '2026-08-19T05:19:31+00:00'
license: 'CC BY-SA 4.0'
---

# Enterprise Federation

Checking an identity carried by an assertion through signature, issuer, audience, and duration window; preventing reuse; mapping externally supplied attributes to a local role with an explicit table; and the limit of logout.

The previous lesson added the identity layer on top of the authorization delegation
protocol: the application learned who the user was from a signed document. The party
that issued the document and the party that consumed it were parts of the same
organization.

In enterprise federation, this assumption is dropped. The user authenticates their
identity **at their own organization**; the library service accepts the user by
trusting the document that organization issued. The library never sees the password
and never keeps a user list. The name of this arrangement is **single sign-on**, and
its carrier is the **assertion**.

## The Direction and Limit of Trust

Federation has two parties: the **identity provider** that authenticates the
identity, and the **service provider** that manages access to the resource. Trust is
one-directional and limited: the service provider trusts the identity provider's
claim of "this person is who they are"; it makes the decision of "this person can do
this" itself.

This distinction is the cross-organization form of the distinction in the course's
first lesson. Identity comes from outside, authorization stays inside. If an
externally supplied attribute is translated directly into authorization, the
neighboring organization's group management ends up determining our authorization
system.

The account below runs the issuance, verification, and mapping of an assertion to a
local role end to end.

```bash
cat > federation.mjs <<'EOF'
// federation.mjs — issuing, verifying, and mapping an assertion to a local role
import { createHmac, timingSafeEqual, randomBytes } from "node:crypto";

const SECRET = Buffer.from("org-signing-key-example");
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64url");
const sign = (body) => createHmac("sha256", SECRET).update(body).digest("base64url");

// The identity-providing organization issues an assertion.
function issueAssertion({ subject, emailVerified, groups, audience, seconds, time }) {
  const body = b64({
    iss: "https://auth.north-university.test",
    sub: subject, aud: audience,
    iat: time, exp: time + seconds,
    jti: randomBytes(6).toString("hex"),
    email_verified: emailVerified, groups,
  });
  return body + "." + sign(body);
}

// The service-providing organization verifies the assertion. Each step is a separate decision.
const USED = new Set();
function verify(assertion, { audience, now, issuer }) {
  const [body, signature] = assertion.split(".");
  const expected = sign(body);
  if (signature.length !== expected.length ||
      !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) return "reject: signature";
  const claims = JSON.parse(Buffer.from(body, "base64url"));
  if (claims.iss !== issuer) return "reject: issuer";
  if (claims.aud !== audience) return "reject: audience";
  if (now >= claims.exp) return "reject: expired";
  if (now < claims.iat) return "reject: future-dated";
  if (USED.has(claims.jti)) return "reject: reuse";
  USED.add(claims.jti);
  return "accept";
}

const TIME = 1_770_000_000;                       // fixed time: so the output is reproducible
const AUDIENCE = "https://library.north-university.test";
const ISSUER = "https://auth.north-university.test";
const base = { subject: "std-4417", emailVerified: true, groups: ["student", "science-faculty"],
                audience: AUDIENCE, seconds: 300, time: TIME };

const trials = [
  ["valid assertion", issueAssertion(base), { audience: AUDIENCE, now: TIME + 10, issuer: ISSUER }],
  ["written to another org", issueAssertion({ ...base, audience: "https://other.test" }), { audience: AUDIENCE, now: TIME + 10, issuer: ISSUER }],
  ["expired", issueAssertion({ ...base, seconds: 5 }), { audience: AUDIENCE, now: TIME + 60, issuer: ISSUER }],
  ["signature broken", issueAssertion(base).slice(0, -1) + "x", { audience: AUDIENCE, now: TIME + 10, issuer: ISSUER }],
  ["from another issuer", issueAssertion(base), { audience: AUDIENCE, now: TIME + 10, issuer: "https://fake.test" }],
];
console.log("trial                      result");
for (const [name, assertion, condition] of trials) console.log(`  ${name.padEnd(24)} ${verify(assertion, condition)}`);

// If the same assertion is presented twice
const again = issueAssertion(base);
console.log(`  ${"first presentation".padEnd(24)} ${verify(again, { audience: AUDIENCE, now: TIME + 10, issuer: ISSUER })}`);
console.log(`  ${"same assertion again".padEnd(24)} ${verify(again, { audience: AUDIENCE, now: TIME + 20, issuer: ISSUER })}`);

// Externally supplied attributes are mapped to a local role; the mapping table must be explicit.
const MAPPING = [
  { condition: (c) => c.groups.includes("library-staff"), role: "clerk" },
  { condition: (c) => c.groups.includes("student") && c.email_verified, role: "member" },
  { condition: (c) => c.groups.includes("faculty"), role: "member" },
];
function localRole(assertion) {
  const c = JSON.parse(Buffer.from(assertion.split(".")[0], "base64url"));
  const matched = MAPPING.find((m) => m.condition(c));
  return { subject: c.sub, groups: c.groups.join(","), role: matched ? matched.role : "no role" };
}
console.log("\nexternal subject           groups                      local role");
for (const groups of [["student", "science-faculty"], ["library-staff"], ["guest"]]) {
  const result = localRole(issueAssertion({ ...base, groups }));
  console.log(`  ${result.subject.padEnd(24)} ${result.groups.padEnd(27)} ${result.role}`);
}
const unverified = localRole(issueAssertion({ ...base, emailVerified: false, groups: ["student"] }));
console.log(`  ${unverified.subject.padEnd(24)} ${"student (email not verified)".padEnd(27)} ${unverified.role}`);
EOF
node federation.mjs
```

```text
trial                      result
  valid assertion          accept
  written to another org   reject: audience
  expired                  reject: expired
  signature broken         reject: signature
  from another issuer      reject: issuer
  first presentation       accept
  same assertion again     reject: reuse

external subject           groups                      local role
  std-4417                 student,science-faculty     member
  std-4417                 library-staff               clerk
  std-4417                 guest                       no role
  std-4417                 student (email not verified) no role
```

## Verification Is Not One Step

The output's first section shows five separate rejections, and each comes from a
separate check. The signature says the document was not altered in transit — but it
is not sufficient on its own. The issuer check verifies that the document came **from
the expected organization**; if this check is skipped, any organization capable of
issuing a validly signed document can identify our users.

The audience check is the one most often skipped. The identity provider issues
documents for more than one service for the same user; if a document issued for one
service is accepted at another, a weakly protected service becomes a gateway into a
strongly protected one. The document's audience states who is meant to accept it.

The duration window is two-sided: the past side checks the document's lifetime, the
future side checks clock skew. Keeping the window narrow shortens the usable time if
the document is intercepted.

## The Same Document Cannot Be Used Twice

The second section's two lines show the reuse defense. The document carries a unique
identifier, and this identifier is recorded when the document is consumed; the same
document is rejected when presented a second time.

The record's lifetime is as long as the document's lifetime: storing the identifier
of an expired document is unnecessary, because the duration check already rejects it.
This keeps the record from growing without bound — the reuse list is a record that
looks back only as far as the validity window.

## The Attribute-to-Role Mapping Must Be Explicit

The third section runs the mapping table. Externally supplied group names are not
used directly as a role; they are translated according to a local table. A group with
no match gives a "no role" result, and the user cannot access the resource.

The last line shows an additional condition: being in the student group is not
enough — the email must also be verified. Not every attribute the identity provider
sends carries the same reliability; which attribute to trust under which condition is
the service provider's decision.

The practical consequence of the table being explicit is this: when the neighboring
organization opens a new group, nothing changes in our system. Authorization only
expands when a row is added to the mapping table, and that row passes through review.

## The Limit of Logout

In federation, logout is not one-sided. When the user logs out of the library
service, their session at the identity provider persists; the redirect on the next
request silently issues a new document, and the user appears never to have logged
out.

This has two counterparts. A local logout ends only the session at the service
provider and does not satisfy a "sign in with another account" request. A central
logout also ends the session at the identity provider and affects every service tied
to that provider. Shared machines require the second one; the user must be told
plainly which one was done.

## Summary

- In federation, identity comes from outside and authorization stays inside: the
  assertion states who the person is, and the service provider determines what they
  can do.
- Verification is not one step; signature, issuer, audience, and duration window are
  each checked separately — in the model, each of these checks produced a separate
  rejection.
- The document is consumed once with its unique identifier; the reuse list looks back
  only as far as the document's validity window.
- Externally supplied attributes are translated into a local role with an explicit
  table; an unmatched group gets no authorization, and the neighboring organization
  opening a group does not change our authorizations.
- Local logout and central logout are different things; which one was done must be
  communicated to the user.

## Next Step

In every flow up to this point, there was a human on the other end: someone entering
a password, reading the consent screen, providing the second factor. But some of the
callers to the library service are not human — a batch job running overnight, the
neighboring organization's catalog syncer, a dashboard. None of these has a password
or a consent screen. The next lesson addresses the credential used for these callers:
how it is issued, where it is stored, and how it is rotated.
