Question- requests not going through SessionAuthenticator middleware

Hello! I'm currently working on integrating a user authentication flow using SotoCognito and Vapor's SessionAuthenticators to persist authentication. I have a breakpoint in authenticate in UserSessionAuthenticator, but my requests never pass through there, so it's always redirecting to the login page. What might be happening here?
Here's a snippet of the code:

func routes(_ app: Application) throws {
    let authenticated = app.routes.grouped([app.sessions.middleware, UserSessionAuthenticator()])
    let portalRedirect = authenticated.grouped(AuthenticatedUser.redirectMiddleware(path: "login"))

    authenticated.post("login", use: login)
    authenticated.get("login") { req async throws in
        try await req.view.render("login", ["title": "Login"])
    }
    portalRedirect.get("portal") { req async throws in
        try await req.view.render("portal", ["title": "Portal"])
    }
}

func login(_ req: Request) async throws -> Response {
    let user = try req.content.decode(User.self)
    let response = try await req.application.cognito.authenticatable.authenticate(username: user.email, password: user.password, context: req)
    switch response {
    case .authenticated(let authenticatedResponse):
        req.auth.login(AuthenticatedUser(sessionID: authenticatedResponse.accessToken!))
    default:
        ...
    }
    throw Abort.redirect(to: "portal")
}

struct User: Content {
    var email: String
    var password: String
}

struct AuthenticatedUser: SessionAuthenticatable {
    var sessionID: String
}

struct UserSessionAuthenticator: AsyncSessionAuthenticator {
    typealias User = AuthenticatedUser
    func authenticate(sessionID: String, for request: Vapor.Request) async throws {
        do {
            // TODO: handle response, this is to check if token is still valid
            let response = try await request.application.cognito.authenticatable.authenticate(accessToken: sessionID, on: request.eventLoop)
            request.auth.login(User(sessionID: sessionID))
        } catch let error as SotoCognitoError { 
            ...
        }
    }
}

In configure.swift

app.sessions.use(.memory) // for now

Thanks so much!

I might be wrong, but I think this is happening because nothing is persisted in the session on the client yet (i.e. the vapor_session cookie is not set). From the docs:

If the authentication has not been persisted in the session yet, the request will be forwarded to the next authenticator.

Can you confirm if this cookie is set on the request?

Yes, I do see the vapor_session cookie upon getting the login page (I've since changed authenticated.get("login") to app.get("login") so now the cookie is only generated when the user submits the login form).

In the login middleware you need to authenticate the session so that it gets persisted to the next request with that session ID

Is that not what this does?

case .authenticated(let authenticatedResponse):
        req.auth.login(AuthenticatedUser(sessionID: authenticatedResponse.accessToken!))

No that only adds the users to the current request's authentication cache. You want something like

request.session.authenticate(AuthenticatedUser(...))

Then will get picked up by the session middleware on the way out and ensure it's saved

Ahh, I understand! I added that in the login route but the behavior remains the same (portal redirects to login and not hitting the authenticate breakpoint in UserSessionAuthenticator).

Does it hit the middleware at all? What is the response the server returns?

It only hits the redirect middleware, but never the user session authenticator. In login, the request goes through the authenticated case, then redirects to portal. The get request to portal then hits the redirect middleware and the login page is displayed again.
Specifically:

[ INFO ] POST /login [request-id: 38BCDF6E-6238-43A3-9D97-7443606139FB]
[ WARNING ] Abort.303: See Other [request-id: 38BCDF6E-6238-43A3-9D97-7443606139FB]
[ INFO ] GET /portal [request-id: 1D2763DC-03E1-4B66-A564-961A2E8D9899]
[ INFO ] GET /login [request-id: 80AE9F2A-3B25-4C52-AA5F-A1E4BC8BA6A9]

You'll need to put a breakpoint inside the conformance of AsyncSessionAuthenticator to AsyncMiddleware to see why it's not even running through the authenticator (e.g. it can't find a session ID), inspect the request and then work backwards from there

Thank you for your help Tim! It was an issue with throw Abort.redirect(to: "portal"). Changing it to req.redirect(to: "portal") fixes the issue.