Universal Login with Ktor server

I’m currently building a Ktor server that handles two responsibilities:

  1. It operates an API that requires login to access.
  2. It handles routing for the front end website.

Setting up jwt auth on the API was fairly straightforward, using this blog post as a guide: Adding Auth0 Authorization to a Ktor HTTP API . However, setting up the routes for /login , /logout and /callback are proving more challenging. There is no dedicated Ktor quickstart, so I looked into the java quickstart. Unfortunately the Java MVC Commons seems to be built around the HttpServletRequest and HttpServletResponse interfaces, while Ktor utilizes its own RoutingCall class (which has its own Request/Response interfaces).

I looked into lifting the logic out of the AuthenticationController#buildAuthorizeUrl and AuthenticationController#handlemethods, but it felt like the servlet interfaces were too embedded for that to be an easy job, or a wise idea.

I was wondering if there was any existing Ktor quickstart, or a guide somewhere on how to build a shim to the existing java quickstart that would allow implementation of Universal Login?

Thanks

Hi @peter.attardo

Welcome to the Auth0 Community!

I can see that you are having struggles in building a Ktor application with Auth0.

Trying to write a custom servlet-shim is highly discouraged, as Ktor provides a built-in, native OAuth Plugin (io.ktor:ktor-server-auth). This plugin handles OIDC/OAuth2 Authorization Code Grant flows (Universal Login) completely out of the box.

By utilizing Ktor’s native plugin instead of the Java MVC SDK, you can integrate directly with Auth0’s Universal Login endpoints while maintaining native, non-blocking RoutingCall performance.

  1. : Install Dependencies

Add the native Ktor authentication plugin to your build dependencies (build.gradle.kts):

implementation("io.ktor:ktor-server-auth:$ktor_version")
implementation("io.ktor:ktor-server-sessions:$ktor_version") 

  1. Configure Sessions and Universal Login

In your Ktor application entry point (typically Application.module), install the Sessions and Authentication plugins:

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*

data class UserSession(val token: String, val idToken: String) : Principal

fun Application.module() {
    install(Sessions) {
        cookie<UserSession>("user_session") {
            cookie.path = "/"
            cookie.maxAgeInSeconds = 3600 // 1 hour
        }
    }

    val httpClient = HttpClient(CIO)

    install(Authentication) {
        oauth("auth0-oauth") {
            urlProvider = { "http://localhost:5173/callback" }
            providerLookup = {
                OAuthServerSettings.OAuth2ServerSettings(
                    name = "auth0",
                    authorizeUrl = "https://{{tenant_domain}}.us.auth0.com/authorize", 
                    accessTokenUrl = "https://{{tenant_domain}}.us.auth0.com/oauth/token",
                    clientId = "Client_ID",
                    clientSecret = "YOUR_CLIENT_SECRET",
                    accessTokenRequiresBasicAuth = false,
                    requestMethod = HttpMethod.Post,
                    defaultScopes = listOf("openid", "profile", "email") /
                )
            }
            client = httpClient
        }
    }
}

  1. Implement Native /login, /callback, and /logout Routes

By wrapping routes in the Ktor authenticate block, the platform handles the authorization redirect challenge on /login and validates the incoming state parameter on /callback automatically:

routing {
    get("/") {
        call.respondText("Welcome to the website!")
    }

    authenticate("auth0-oauth") {
        get("/login") {
        }

        get("/callback") {
            val principal = call.principal<OAuthAccessTokenResponse.OAuth2>()
            
            if (principal != null) {
                call.sessions.set(UserSession(
                    token = principal.accessToken,
                    idToken = principal.extraParameters["id_token"] ?: ""
                ))
                call.respondRedirect("/dashboard")
            } else {
                call.respondText("Authentication failed.", status = HttpStatusCode.Unauthorized)
            }
        }
    }

    get("/logout") {
        call.sessions.clear<UserSession>()
        
        val domain = "tenant_domain.us.auth0.com"
        val clientId = "client_id"
        val returnTo = "http://localhost:5173" 
        call.respondRedirect("https://$domain/v2/logout?client_id=$clientId&returnTo=$returnTo")
    }

    get("/dashboard") {
        val session = call.sessions.get<UserSession>()
        if (session != null) {
            call.respondText("Hello authenticated user! Your Access Token is: ${session.token}")
        } else {
            call.respondRedirect("/login")
        }
    }
}

All these resources should be available thorough Ktor’s OAuth Documentation.

Kind Regards,
Nik

That is a much more straightforward implementation. Thank you for the reference.