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.
The three endpoints that carry the whole integration
Strip everything else away and the integration is three Tessitura calls. That is it.
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.
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
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.
- Auth0 login via Google, redirecting to the Australia-region tenant and returning a valid token
- Auth0 login via email and password through Universal Login
- A real Tessitura session created by a direct API call, returning a live
sessionKey - Login/External binding a test email to its constituent, returning the
constituentId - Auto-registration creating a constituent when one did not exist
- The
tessitura_sessionclaim embedded in the issued token - A server-side fallback bind returning the full session when called directly
- The whole thing working with no reverse proxy anywhere in the path
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.