Web apps built for Georgia Tech students or staff usually need to let people log in with their GT accounts. GT runs CAS (Central Authentication Service) for this, and the protocol is simple enough to implement by hand in an afternoon. This post walks through the whole thing: the endpoints, the ticket validation flow, and the errors you’ll hit along the way.

What is CAS?

CAS is a single sign-on protocol from the early 2000s: users authenticate once against a central server, and every participating application trusts that login. Georgia Tech runs CAS 3.0 at sso.gatech.edu.

The appeal for you as an app developer is that your application never sees a password. You send the user to GT’s login page, GT sends them back with a ticket, and you verify that ticket server-side. Password handling, MFA, and session policy all stay GT’s problem, and users who are already signed in to another GT app skip the login page entirely.

Two kinds of tickets are involved. The service ticket (ST) is a one-time-use token your app receives in the callback and must validate within seconds. The ticket granting ticket (TGT) is a session cookie on the CAS server itself; your app never sees it, but it’s what makes the “already logged in elsewhere” case work.

GT SSO endpoints

PurposeURL
Loginhttps://sso.gatech.edu/cas/login?service={url}
Validate (CAS 2.0)https://sso.gatech.edu/cas/serviceValidate
Validate (CAS 3.0)https://sso.gatech.edu/cas/p3/serviceValidate
Logouthttps://sso.gatech.edu/cas/logout

Use the CAS 3.0 endpoint (/cas/p3/serviceValidate). The 2.0 one validates tickets fine, but only 3.0 returns user attributes like email and display name.

The authentication flow

In words: the user hits a protected page, and you redirect them to GT SSO with a service parameter pointing back at your callback URL. They log in with their GT credentials, CAS redirects to your callback with a one-time ticket, and your server validates that ticket against the CAS server. If it checks out, the user is authenticated and you start a session.

Implementation

Step 1: Redirect to CAS login

Construct the login URL with your service URL as a parameter:

https://sso.gatech.edu/cas/login?service=https://yourapp.com/auth/callback

Example in JavaScript:

function redirectToCAS() {
  const casLoginUrl = "https://sso.gatech.edu/cas/login";
  const serviceUrl = encodeURIComponent("https://yourapp.com/auth/callback");
  window.location.href = `${casLoginUrl}?service=${serviceUrl}`;
}

One thing to get right early: the service URL you send here must match the one you use during validation, character for character. Mismatches are the classic source of INVALID_TICKET errors later.

Step 2: Handle the callback

After authentication, CAS redirects to your callback URL with a ticket:

https://yourapp.com/auth/callback?ticket=ST-12345-abcdefg-sso

Step 3: Validate the ticket

Make a server-side request to validate the ticket:

async function validateTicket(ticket, serviceUrl) {
  const validateUrl = new URL("https://sso.gatech.edu/cas/p3/serviceValidate");
  validateUrl.searchParams.set("ticket", ticket);
  validateUrl.searchParams.set("service", serviceUrl);

  const response = await fetch(validateUrl.toString());
  const xmlText = await response.text();

  return parseXMLResponse(xmlText);
}

Step 4: Parse the response

A successful authentication returns XML like this:

<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
  <cas:authenticationSuccess>
    <cas:user>gburdell3</cas:user>
    <cas:attributes>
      <cas:mail>george.burdell@gatech.edu</cas:mail>
      <cas:displayName>George P. Burdell</cas:displayName>
    </cas:attributes>
  </cas:authenticationSuccess>
</cas:serviceResponse>

A failed authentication looks like this:

<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
  <cas:authenticationFailure code="INVALID_TICKET">
    Ticket ST-12345-abcdefg-sso not recognized
  </cas:authenticationFailure>
</cas:serviceResponse>

Here is a simple parser:

function parseCASResponse(xmlText) {
  const userMatch = xmlText.match(/<cas:user>([^<]+)<\/cas:user>/);
  if (userMatch) {
    return {
      success: true,
      user: userMatch[1],
    };
  }

  const failureMatch = xmlText.match(
    /<cas:authenticationFailure[^>]*>([^<]*)<\/cas:authenticationFailure>/
  );
  if (failureMatch) {
    return {
      success: false,
      error: failureMatch[1].trim() || "Authentication failed",
    };
  }

  return { success: false, error: "Invalid CAS response" };
}

Step 5: Create a session

After validating the ticket, create your own application session:

app.get("/auth/callback", async (c) => {
  const ticket = c.req.query("ticket");
  const result = await validateTicket(ticket, serviceUrl);

  if (result.success) {
    const session = await createSession(result.user);

    setCookie(c, "session", session.id, {
      httpOnly: true,
      secure: true,
      sameSite: "Lax",
    });

    return c.redirect("/dashboard");
  }

  return c.redirect("/login?error=auth_failed");
});

Common errors

“Service not authorized”

Your service URL is not registered with GT’s CAS server. Contact Georgia Tech IT to register your application’s callback URL.

“INVALID_TICKET”

The ticket was already used (they’re single-use), it expired (they live for something like 5-10 seconds), or the service parameter differs between the login redirect and the validation request. Check the last one first.

“INVALID_SERVICE”

The service URL format is off. Use HTTPS, URL-encode the parameter properly, and be consistent about trailing slashes.

Redirect loop

If your app keeps bouncing users back to CAS after a successful login, ticket validation is succeeding but your session isn’t being created or read correctly. The CAS side is done at that point; debug your cookie and session handling.

Security notes

Validate tickets server-side, every time. A client-side check proves nothing, since anyone can fabricate the response. Validate the ticket immediately in the callback (it expires in seconds), issue your own session cookie, and never touch the ticket again: don’t store it, don’t reuse it, and keep it out of your logs. It’s worth logging the authentication event itself, who logged in and when, just not the ticket value. Finally, define your service URL in one place; the login and validation values drifting apart is the easiest way to break this integration.

Resources