Solution Architecture  /  Identity  /  Powerhouse DXP

One session key,
two systems.

How we connected Auth0 to Tessitura TNEW ticketing with nothing but a post-login hook and a REST call. As far as I can tell, the first time it has been wired this way in Australia.

AUTH0 Universal Login JWT tessitura_session constituent_id as claims TNEW book.powerhouse sessionKey ONE AUTHENTICATION · ONE KEY · TWO SYSTEMS IN STEP

There is a particular kind of problem that looks simple in the meeting and turns out to be the whole project. Ours was this: a patron signs in once, and two completely separate systems both need to believe them. Auth0 owns the login. Tessitura owns the tickets, the memberships and the money. Neither one was built to trust the other.

This is the story of how we joined them for Phase 1 of the Powerhouse Digital Experience Platform, with the ticketing front end (TNEW, sitting at book.powerhouse.com.au) as the first production target. The short version is that the answer was far smaller than everyone expected, and the long way round that most people assume you need turned out to be unnecessary. I want to walk through both, because the wrong turn is as instructive as the fix.


01 · THE PROBLEMTessitura has no login page of its own

Tessitura is the constituent database at the centre of the museum. Every patron is a constituent record with a numeric ID, and that ID is the primary key for who a person is across ticketing, membership and CRM. TNEW is Tessitura's own web storefront for buying tickets.

Here is the awkward part. Tessitura does not ship a single sign-on login screen. So once you decide that Auth0 will run authentication, in front of a nice branded Universal Login with social sign-in and passwordless, you are left holding three questions:

If Auth0 draws the login box, how does Tessitura ever learn that the person is authenticated? How does a Tessitura session get created and kept alive after the Auth0 login finishes? And what, precisely, tells TNEW that this browser is now a logged-in constituent rather than an anonymous shopper?

Everyone I spoke to early on had an answer, and it was the same answer, and it was wrong.

02 · THE WRONG TURNThe reverse proxy that everyone insisted on

The received wisdom, pushed hardest by the reseller advising on the licence, was that you need a reverse proxy in front of TNEW. Something like an Okta Access Gateway, sitting in the request path, injecting headers, translating a token into whatever Tessitura would accept. The reasoning sounds tidy. TNEW cannot speak modern identity protocols, so you put a translator in front of it.

I did not buy it. A reverse proxy in the hot path of every ticket purchase is a new single point of failure, a new thing to scale on a school-holiday spike, a new box to patch, and a new place for a session to get mangled. It is the kind of component you accept only when there is genuinely no other way. My position was that we should prove there was no other way before we committed to running one.

Architecture decision · ADR-006

The reverse proxy and Access Gateway approach was formally rejected. Not on preference, on evidence. We built a proof of concept that authenticated real test constituents against the Tessitura test environment with direct REST calls and no proxy in sight. Once that was running, the argument was over.

The lesson I keep relearning: when a vendor tells you a heavy component is mandatory, ask them to show you the specific API call that cannot be made without it. If they cannot name it, the component is probably optional.

03 · THE KEY INSIGHTLogin/External, the endpoint built for exactly this

I went through the Tessitura REST API specification properly, and the whole knot came undone on a single endpoint. Tessitura exposes a login method that takes a username and no password.

POST /api/Web/Session/{sessionKey}/Login/External
{
  "userName": "patron@email.com",
  "loginTypeId": 1
}
// note: there is no password field on this endpoint

From the Tessitura REST API specification, v16.0.21.

Read the intent there. This method exists specifically for external authentication systems that have already verified who the person is. Tessitura is saying, in effect, I trust that you did the login somewhere else, just tell me which constituent to bind this session to. That is precisely the contract Auth0 wants. Auth0 confirms identity, then hands Tessitura the email, and Tessitura attaches the session to the right record.

No password crosses the boundary. No proxy sits in the path. The whole integration lives in a few lines of code that run inside the login itself.

Those few lines are an Auth0 Post-Login Action. It is a small piece of JavaScript that runs server side, inside Auth0, in the moment after the person authenticates and before Auth0 issues its token. It can make outbound HTTPS calls and it has an encrypted secrets vault. Which means it can talk to Tessitura directly. That is the entire trick.


04 · THE ARCHITECTUREWhat actually sits in the path

Here is the shape of the whole thing. The important read is what is not there. No gateway, no header-injecting proxy, no bespoke middleware cluster. Auth0 calls Tessitura, embeds the result in its own token, and a small session service reads that token and hands the browser off to TNEW. CosmosDB sits underneath as the register that remembers who a patron is across systems.

Patron browser AUTH0 · phm.au.auth0.com Universal Login email · social · passwordless · MFA Post-Login Action runs server-side, before token issue calls Tessitura · writes JWT claims TESSITURA REST API POST /Web/Session POST /Session/{key}/Login/External POST /Registration/{key}/Register system of record · constituent identity SESSION-SERVICE · Vercel reads JWT claims · fallback bind builds signed TNEW handoff URL /callback · /tessitura-bind · /tnew-handoff TNEW · book.powerhouse.com.au validates sessionKey against Tessitura member pricing · details pre-filled COSMOSDB · phm-identity Auth0 user id ⟷ constituent id ⟷ (Phase 2: Shopify, Mews, member id) direct REST, no proxy JWT with claims redirect + sessionKey
The settled architecture. The Post-Login Action is the only new thing in the identity path. Everything else is either a system that already existed (Auth0, Tessitura, TNEW) or a thin service that reads a token and redirects. The register at the bottom keeps Auth0 clean by holding the cross-system mapping outside the identity provider.

The three endpoints that carry the whole integration

Strip everything else away and the integration is three Tessitura calls. That is it.

/api/Web/SessionCreate an anonymous session, get back a sessionKey. Like grabbing a basket before you have logged in.
POST
/api/Web/Session/{sessionKey}/Login/ExternalBind that session to a constituent by email, no password. The one that makes the whole thing possible.
POST
/api/Web/Registration/{sessionKey}/RegisterCreate a brand new constituent and web login on first sign-up.
POST

05 · THE FLOWOne login, start to finish

Put the pieces in motion and this is the sequence a patron sets off when they click sign in. Six steps, most of them invisible to the person, all of them finished in the time it takes a page to redirect.

Patron Auth0 Action Tessitura TNEW 1 clicks Sign In, redirect to Universal Login 2 verifies identity 3 Action fires before token issued create session, get sessionKey 4 Login/External by email, bind constituent isLoggedIn: true · constituentId 5 sessionKey + constituentId written as JWT claims token returned to session-service 6 redirect to TNEW with sessionKey on the URL logged in
The six-step handoff. Steps 3 and 4 happen entirely inside Auth0, server-side, in the pause between the person authenticating and the token being minted. By step 6 the patron lands on TNEW already recognised, with member pricing applied and their details pre-filled.

Written out as code, the heart of the Action is short enough to read in one sitting.

// Auth0 Post-Login Action (abridged)
exports.onExecutePostLogin = async (event, api) => {

  // 1. anonymous Tessitura session
  const { sessionKey } = await tessitura("POST", "/Web/Session");

  // 2. bind the constituent by email, no password
  const bind = await tessitura("POST",
    `/Web/Session/${sessionKey}/Login/External`,
    { userName: event.user.email, loginTypeId: 1 });

  // 3. embed the result as claims on Auth0's own token
  api.idToken.setCustomClaim("tessitura_session", sessionKey);
  api.idToken.setCustomClaim("tessitura_constituent_id", bind.constituentId);
};

The full Action also handles first-time patrons and the in-person buyer case, covered next.


06 · THE DETAIL THAT SEPARATES REAL FROM DEMOThree states a patron can be in

The happy path assumes the patron already has a web login. In a real patron base they very often do not, and this is the single detail that tells me whether someone has actually thought the integration through or just run a demo. The Post-Login Action has to handle three distinct states on first sign-in through Auth0.

Exists, with a web login STATE 1

The patron registered online before. This is the easy one. Call Login/External directly and you are done.

action: Login/External

Exists, no web login STATE 2

The one that gets missed. This person bought tickets in person or over the phone. They are a real constituent in Tessitura, but they have never registered online, so Login/External fails outright. The Action has to detect that, create a web login for them first, then bind.

action: create web login, then Login/External

Does not exist STATE 3

A brand new patron the museum has never seen. Run a full registration to create the constituent and web login, then bind.

action: Register, then Login/External

Patron signs in look up constituent STATE 1 · has web login Login/External STATE 2 · in-person buyer create web login, then Login/External STATE 3 · brand new Register, then Login/External Bound session same key for all three
All roads reach the same session. Whichever state the patron starts in, the Action ends with one bound Tessitura session and one constituent id, ready to travel on the token. State 2 is the one that separates a design that survives contact with a real audience from one that does not.

The placeholder password, and why it never leaves the room

State 2 and State 3 both have to create a web login, and Tessitura insists on a password field when you do. Auth0 handles the actual authentication, so this password is never used to log anyone in. It exists only to satisfy a mandatory field.

That is a small thing that becomes a security rule the moment you are careless with it. So the rule is strict. The Action generates a cryptographically random 32-character string, uses it once for that one field, and then it is gone. It is never stored, never returned to the browser, never written to the identity register, never put in a log line, and never placed in the token. If you ever find that value sitting anywhere outside the few milliseconds of the Action running, something has gone wrong.

Why it matters

A placeholder credential that leaks is worse than no credential, because it looks legitimate. Keeping it inside the execution context, and nowhere else, is what keeps it harmless.


07 · THE HANDOFFGetting the key into TNEW

By now Auth0 has done its job and the token carries the session details as claims. A patron in front of TNEW is still just an anonymous shopper until TNEW is handed the key. Tessitura supports two ways to share a session. There is a cookie method, and there is a redirect method. Because powerhouse.com.au and book.powerhouse.com.au sit under the same parent domain, the cookie method would technically work. We went with the redirect method anyway, because it is what Tessitura recommends for new projects, and their senior consultant confirmed it is the safer choice.

A small service, which the team calls session-service, reads the claims off the token and builds a signed handoff URL. TNEW is configured to accept an external session key on the URL, validates it against Tessitura, and from that point the browser is a logged-in constituent inside TNEW. The service also carries a server-side fallback bind, so if the Post-Login Action ever fails to complete cleanly, there is a second path to recover the session rather than dumping the patron back to a login screen.

The one thing worth flagging for anyone doing this: the session key that travels is encrypted, and the encryption has to match Tessitura's own sample exactly. AES-256-CBC with PKCS7 padding, a key derived with PBKDF2, and an HMAC-SHA256 signature over the payload. Get one parameter wrong, the salt, the iteration count, the byte order of the message, and TNEW rejects the session with an error that tells you almost nothing. We ported their C# sample to Node byte for byte, and only then did it validate.

One DNS trap worth the warning

Point book.powerhouse.com.au at the Tessitura hostname in DNS-only mode. Do not proxy it through a CDN or front door. Proxying breaks TNEW in ways that cost a day to diagnose, because the failure looks like an application bug rather than a routing one.

08 · THE REGISTERWhere a patron's identity lives across systems

One decision I want to defend, because it is easy to skip. Auth0 is the identity provider. It is not the identity database. The moment you start stuffing cross-system mappings into your login provider, you have made it the thing every other system depends on, and you have coupled your identity data to your authentication vendor.

So underneath Auth0 sits a small CosmosDB register, phm-identity. Each record holds the Auth0 user id and the Tessitura constituent id, with fields held in reserve for the Shopify customer id, the Mews guest id and the member id, for when Phase 2 brings those systems online. It gives us one honest answer to the question, who is this patron across everything we run, without asking Auth0 to be something it is not.

Tessitura stays the system of record for who a person is. Auth0 only ever answers whether they are who they claim to be. Keeping those two jobs apart is most of the discipline in this design.


09 · THE PROOFWhat we actually stood up, not what we hoped for

None of the above is a whiteboard argument. Before a line of production code was written, we ran the full pattern against the Tessitura test environment and against a live Auth0 tenant in the Australia region. Here is what came back green.

That last line is the one that ended the debate. Once you can point at a working session, created by a direct REST call from inside the login, the question of whether you need a gateway answers itself.

← All writing rummanahmed.com Share on LinkedIn