How to authenticate with web form in Ktor?

I could not find a working example of authenticating with web form. Here is my setup.

install(Authentication) {
        form("myFormAuth") {
            userParamName = "username"
            passwordParamName = "password"
            challenge = FormAuthChallenge.Redirect{ "/login" }
            validate { if (it.name == "test" && it.password == "password") UserIdPrincipal(it.name) else null }
        }
}

Then I can secure a route like this

authenticate("myFormAuth") {
        get("/protected/route/form") {
            call.respondText("Hello form auth")
        }
}

But how can I actually authenticate a user? What does a [POST] route/action that handle authentication look like? Should it set some cookie session?

Here is an example of using form authentication, but (i) it does not install/register an authentication mechanism, and (ii) we don’t see how it would handle a successful and failed authenticating attempt.

Could someone provide a better example?

Thanks.

Pretty sad that such a question receives no replies.

Anyway, I think I found the solution in case someone has the same problem.

The idea is that authentication mechanism in Ktor is responsible for validating a login request, so a route encapsulated by authenticate(“my-auth-name”) will also attempt to find credential data (username/password) in the request. It does not do anything special, like marking authenticate status in session. It is up to us to do that. So I updated my code with the following 2 steps:

  1. In my post("/login") route, set a user session upon successful authentication, i.e. principal is not null.
data class AuthenticatedUser(val name: String)

//...//

authenticate("myFormAuth") {
        post("/login") {
              val principal = call.principal<UserIdPrincipal>() ?: error ("no auth found")
              call.sessions.set(AuthenticatedUser(principal.name))
        }

        get("/protected/route/form") {
            call.respondText("Hello form auth")
        }
}
  1. In the authentication mechanism, skip authentication when such a session is present.
install(Authentication) {
        form("myFormAuth") {
            userParamName = "username"
            passwordParamName = "password"
            challenge = FormAuthChallenge.Redirect{ "/login" }
            validate { if (it.name == "test" && it.password == "password") UserIdPrincipal(it.name) else null }
            skipWhen { call -> call.sessions.get<AuthenticatedUser> != null }
        }
}

Hi @kha,

I’m assuming you’ve moved on from this by now, but for others (because this was a top search result), I’m going to document a more correct solution, because it’s a bit confusing.

The Authentication module that ktor provides generates two phases: Authenticate and Challenge, inserted in that order after the Features phase. These phases are executed every time a route inside a authenticate {} block is hit. Authenticate means “let’s try to authenticate the user based on the request” and Challenge means “try again at the login page”, essentially. (Naturally Authenticate doesn’t do much if you’re already authenticated)

The point is…that Authenticate phase? Only gets hit from inside an authenticate {} route!

tl:dr; something like this should work:

get("/auth") {
    call.respondHtml {
        body {
            form(action = "/auth", method = FormMethod.post) {
                // a form using userParamName and passwordParamName
                // from when you configured form auth
            }
        }
    }
}

authenticate {
    post("/auth") {
        call.respondRedirect("/user_is_signed_in")
    }
}

Alternatively, if you have a signed-in-only landing page, you can configure your form to POST directly to that URL; the important thing is that where you POST to must be authenticated.

Bonus question…when registering, how do you auto-sign in the user afterwards?

I think the answer is call.authentication.principal(UserIdPrincipal(...)).

1 Like

And…one more thing. How do you handle failed attempts by the user to authenticate? e.g. when do you display “invalid username or password”?

In your Authentication form configuration block:

challenge {
    val errors: Map<Any, AuthenticationFailedCause> = call.authentication.errors
    when (errors.values.singleOrNull()) {
        AuthenticationFailedCause.InvalidCredentials ->
            call.respondRedirect("/auth?invalid")

        AuthenticationFailedCause.NoCredentials ->
            call.respondRedirect("/auth")

        else ->
            call.respondRedirect("/auth")
    }
}

Then you can check the query parameter in your route.

(In case it’s not obvious, NoCredentials is the branch that’s hit when you try to hit a guarded page without even trying to authenticate first.)

1 Like

Alright, I’ve put together a full sample application that uses cookies and ktor’s auth mechanisms, which you can find here: ktor-session-auth-example. Check it out and let me know what you think!

9 Likes

Thanks a lot for your clarifying examples.