Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Need ability to use a custom sessionID #47

Closed
andrewluetgers opened this issue Jun 4, 2014 · 10 comments
Closed

Need ability to use a custom sessionID #47

andrewluetgers opened this issue Jun 4, 2014 · 10 comments
Milestone

Comments

@andrewluetgers
Copy link

We've been trying to implement multi-app authentication with CAS using connect-cas. We've had issues with that codebase in terms of sso support but have our own implementation based on it now so that library is not an isssue at this point. To support single sign out the CAS server needs to be able to kill a session using a post message back to Express. The CAS protocol sends a session ticket which is not the express sessionID. To make this work with memcache we need to use the CAS session ticket as the sessionID. Others have had similar issues with custom sessionID

@andrewluetgers andrewluetgers changed the title custom sessionID Need ability to use a custom sessionID Jun 4, 2014
@joewagner
Copy link
Member

#40 related

@joewagner
Copy link
Member

In reference to this diagram, when the browser is redirected to the ticket transfer callback URL –#3 in the diagram– you should be able to regenerate the session and set the sessionID. For example:

app.get('/casCallback', function (req, res, next) {
    var ticket = req.query.casTicketName;
    req.session.regenerate(function (err) {
        if (err) // handle error ...
        req.sessionID = ticket;

        // attach stuff to session or call next() and so on ...
    });
});

The ID will be signed with your secret, like usual, then used in the cookie thats set on the browser.
Does this work for your use case?

@andrewluetgers
Copy link
Author

We have struggled to get this to work with the MemoryStore which we have switched to for now as we can easily do a lookup in sessionStore.sessions to find the corresponding sid for a given CAS service ticket. One challenge that we had a hard time diagnosing was that when we hit the logout route and destroy the session, there happens to be a set session with the old sid and user info queued up so I can see the session get destroyed and then recreated right afterward. To solve it we null out req.session and req.sessionId inside the req.session.destroy callback before we redirect back to CAS. I have a feeling there may be a similar issue going on with regenerate but have not had time to dig into it. Do you have any insight into this behavior, as it was pretty unintuitive and seems like a bug in the order of operations. At this point in-memory is a workable solution as our sessions are small in size and number and with the below code everything is working just fine.

// custom cas ssout middleware
// all posts will be tested for samlp tag, this signifies a sso message from cas
var samlRe = /<samlp:SessionIndex>(.*)<\/samlp:SessionIndex>/;
app.post("*", function(req, res, next) {
    if (!req.sessionStore) {
        throw new Error('no session store configured');
    }

    req.ssoff = true;

    var samlMessage = req.rawBody && decodeURIComponent(req.rawBody).match(samlRe),
        st = samlMessage && samlMessage[1];

    if (st) {
        destroySession(st, function(err, sessionId, ses) {
            console.log("CAS destroyed session", sessionId, ses);
            res.send(204);
        });
    } else {
        next();
    }
});

app.use(cas.serviceValidate());
app.use(cas.authenticate());

app.get("/logout", function(req, res) {
    destroySession(req.session.st, function(err, sessionId, ses) {//
        console.log("User destroyed session", sessionId, ses);
        // must null session here as there is a queued set session call for this
        // route if we omit this step the session will be recreated by that set 
        // session call after its deletion
        req.session = req.sessionID = null;
        res.redirect(casOpts.paths.logout);
    });
});

// destroySession serves both manual and remote (via CAS post-back) session 
// destroy, as such it does not assume a session, insted uses the CAS service 
// ticket (st) to do a reverse lookup for the sid to kill
var destroySession = function(st, cb) {
    var session,
        sessionId = _.findKey(sessionStore.sessions, function(sessionString) {
            var ses = JSON.parse(sessionString);
            if (ses.st == st) {
                session = ses;
                return true;
            } else {
                session = null;
                return false;
            }
        });

    if (sessionId) {
        sessionStore.destroy(sessionId, function() {
            cb && cb(null, sessionId, session);
        });
    } else {
        console.log("could not find sessionId for ", st);
        cb("could not find sessionId for " + st);
    }
};

@joewagner
Copy link
Member

The destroySession function you provided destroys the session in the store directly, but does nothing to the req.session object that was created by the express-session middleware. That is why you have to set it to null.
You should use req.session.destroy. This will destroy the session in the store and delete the req.session object so that it isn't saved again when the response is sent.

Generally speaking, if the route is being hit by the user, you should only be interacting with the req.session object. If the route is being hit by the CAS server, requesting a logout, you will want to destroy the session in the store directly.

@joewagner
Copy link
Member

I'm closing this, as it doesn't seem like a bug. Feel free to reopen if you disagree

@andrewluetgers
Copy link
Author

Thanks a lot those suggestions make way too much sense, my code is much more logical now. So that was ultimately a side issue, to move on to a memcache store we will still need to get your earlier suggestion working. I have attempted just that but run into some issues. Have you successfully changed the sid with this technique? Firstly I see three sessions when I reload the page, of course cas is doing some redirecting right away and I'm wondering if that is the cause, when I get redirected back from cas with the ticket, the cas middleware kindly removes that query param for me, another redirect, when we come back we have a cas user and Service Ticket and need to change the session id to the ST. I believe a new session is being regenerated but the new sid is not the value I provided.

var staticFileRe = /^\/assets/;
app.get("*", function(req, res, next) {
    console.log("catchall");
    var p = req.path || "",
        ticket = req.session.st,
        userName = req.session.cas.user,
        session = _.cloneDeep(req.session);

    function respond(req, res) {
        if (p.match(staticFileRe)) {
            next(); // on to 404
        } else {
            // html5 routing rule
            // all urls not in assets return the single page app html
            req.session.user = req.session.user || users.getUser(userName);
            console.log("req.session", req.session);
            res.send(template.index({user: req.session.user}));
        }
    }

    if (!req.session.casSid) {
        req.session.regenerate(function(err) {
            // todo handle err here
            req.sessionID = ticket;
            _.merge(req.session, session);
            req.session.casSid = true;
            respond(req, res);
        });
    } else {
        respond(req, res);
    }
});

@joewagner
Copy link
Member

I did some more investigation, and the technique I suggested won't work. Sorry about that :)
The id property of a Session instance is set to writable: false link. I had forgotten what the defaults for Object.defineProperty were.

You are going to have to destroy then write your own generate function and set the sessionID there.
For example, building off of your last comment:

if (!req.session.casSid) {
    // destroy the old session
    req.session.destroy(function (err) {
        if (err) { /* todo handle err here */ }

        // req.sessionID has to be set before Session is instantiated
        req.sessionID = ticket;
        req.session = new session.Session(req);
        req.session.cookie = new Cookie(session.cookie);

        _.merge(req.session, session);
        req.session.casSid = true;
        respond(req, res);
    });
} else {
    respond(req, res);
}

The above is a hack for sure, but it might work for you?

I'll reopen this as a feature request, related to #40. Sorry for the confusion at first.

@joewagner joewagner reopened this Jun 9, 2014
@andrewluetgers
Copy link
Author

Awesome, that works. Yeah I too saw the code using defineProperty and was kind of lost at that point. Assumed there was some other process going on perhaps to replace the whole object but clearly not. Thanks so much!

@dougwilson dougwilson added this to the 1.4.0 milestone Jun 18, 2014
@dougwilson
Copy link
Contributor

FYI you should not even be setting your session ID as your CAS session ticket ID; if you need to destroy sessions based on ticket ID, you should either use a store that lets you do that look up (this module lets you build your own store) or you need to use a second storage mechanism to map CAS session ticket IDs to your user's browser session IDs.

@andrewluetgers
Copy link
Author

Memcache and Redis both seem (oddly) ill suited to this task, an actual database is what is required. When we moved on to implement SSO in another app, this time a Java Spring app, we ran into the same problem. Basically CAS Single Sign Out has no reference implementation that supports clustering. We would have to similarly rewrite the sign out filter to be backed by a database then we would be able to do the lookup of a session via a service ticket. This all seems too complex and indeed with CAS 4.0 the SSO process has been greatly simplified with support for clustered apps on all platforms via a front-channel sign out. In CAS 3.x saml post messages originating from the CAS server tell each service to destroy any session associated with a given service ticket. In CAS 4.x, upon logout, the user's client is eventually redirected to each service's logout page one after the other until all of their open sessions are killed. We have implemented this approach using CAS 3.x by forcing the logout route of one service to redirect to the other services logout route which will then redirect to the CAS logout route.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants