Anders Kirkeby

03/15/2023, 6:10 PM
Hi all - I'm at a loss when it comes to the OAuth module with Ktor. I've based my code off of the ktor-sample project auth-oauth-google however, I'm using Strava as my OAuth provider. The login route works, redirects to Strava and with the help of some logging I can see that the
is returned, and then the token-request is issued witch results in a 200 OK with both the access and refresh token being returned in the body. So everything seems to be good on the strava-side. However, the subsequent callback to
returns a 401 - which I for the life of me cannot understand. Do any of you have any pointers, or know of any issues with CIO (or something 🤷 ) Code added in the following thread :heart_hands:
import com.cndtns.auth.UserSession
import com.cndtns.scrapers.strava.models.SummaryActivity
import com.cndtns.strava.StravaConfig
import com.cndtns.strava.stravaOAuthRoutes
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.engine.*
import io.ktor.server.html.*
import io.ktor.server.request.*
import io.ktor.server.resources.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import kotlinx.html.a
import kotlinx.html.body
import kotlinx.html.p
import kotlinx.serialization.json.Json
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.lang.invoke.MethodHandles

val applicationHttpClient = HttpClient(CIO) {
    install(Logging) {
        level = LogLevel.BODY
    } // default logging
    install(ContentNegotiation) {
        json(Json {
            ignoreUnknownKeys = true
            isLenient = true

fun main() {
    embeddedServer(io.ktor.server.cio.CIO, port = 8080, module = Application::main).start(wait = true)

private fun Application.main(httpClient: HttpClient = applicationHttpClient) {
    val redirects = mutableMapOf<String, String>()

    install(Resources) // enable @Resource annotations

    install(Sessions) {

    install(Authentication) {
        oauth {
            urlProvider = { "<http://localhost:8080/login/authorization-callback>" }
            providerLookup = {
                    name = "strava",
                    authorizeUrl = StravaConfig.AUTH_URL,
                    accessTokenUrl = StravaConfig.TOKEN_URL,
                    requestMethod = <http://HttpMethod.Post|HttpMethod.Post>,
                    clientId = StravaConfig.clientId,
                    clientSecret = StravaConfig.clientSecret,
                    defaultScopes = listOf("activity:read_all,read_all"),
                    extraAuthParameters = listOf("approval_prompt" to "auto", "response_type" to "code"),
                    passParamsInURL = true,
                    onStateCreated = { call, state ->
                        val redirectUrl = call.request.queryParameters["redirectUrl"] ?: ""
                        redirects[state] = redirectUrl
                        println("Redirect URL for state $state is $redirectUrl")
            client = httpClient

    routing {
        authenticate {
            get("/login/authorization-callback") {
                val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
                    ?: throw Exception("No principal was given")
                val accessToken = principal.accessToken // TODO: Validate
                val state = principal.state ?: throw Exception("No state was given")

                call.sessions.set(UserSession(state = state, token = accessToken))
            get("/login") {

        get("/") {
            call.respondHtml {
                body {
                    p {
                        a("/login") { +"Login with Strava" }

        get("/{path}") {
            val userSession: UserSession? = call.sessions.get()
            if (userSession != null) {
                val activities: List<SummaryActivity> = httpClient.get("<https://www.strava/api/v3/athlete/activities>") {
                    headers {
                        append(HttpHeaders.Authorization, "Bearer ${userSession.token}")
                call.respondText("Here are your activities, ${activities.joinToString(", ") { "${} - ${}" }}!")
            } else {
                val redirectUrl = URLBuilder("<http://localhost:8080/login>").run {
                    parameters.append("redirectUrl", call.request.uri)

Aleksei Tirman [JB]

03/16/2023, 6:55 AM
So does a client get redirected to the
endpoint? If so can you trace what line of your code in that route’s handler causes returning 401?

Anders Kirkeby

03/16/2023, 8:15 AM
No it doesnt - tried setting breakpoints there, but it seems "blocked" by the surrounding "authenticate" block 🤔 It is definitely able to retrieve the access_token seen by logging the body respons on the HTTPClient, but it seems like the principal is never being set. I know the Strava token-endpoint responds with an additional "athlete"-entry, however I cannot find the place where it attempts to sett the token using the httpClient.
Correction, the browser is redirected back to the
page but receives a 401 from the ktor-server
Tried using the google-oauth setup i linked and that seems to work flawless. Noticed that the reponse in the token request is quite different. Google's:
  "access_token": "...",
  "expires_in": 3599,
  "scope": "<>",
  "token_type": "Bearer",
  "id_token": "..."
Not sure how I can change the expected return type however as this seems omitted in the docs 🤔