https://kotlinlang.org logo
#ktor
Title
# ktor
a

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
authorization_code
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
/login/authorization-callback
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 🫶
Copy code
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.call.*
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) {
        cookie<UserSession>("user_session")
    }

    install(Authentication) {
        oauth {
            urlProvider = { "<http://localhost:8080/login/authorization-callback>" }
            providerLookup = {
                OAuthServerSettings.OAuth2ServerSettings(
                    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))
                call.respondRedirect("/")
            }
            get("/login") {
                call.respondRedirect("/")
            }
        }

        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}")
                    }
                }.body()
                call.respondText("Here are your activities, ${activities.joinToString(", ") { "${it.id} - ${it.name}" }}!")
            } else {
                val redirectUrl = URLBuilder("<http://localhost:8080/login>").run {
                    parameters.append("redirectUrl", call.request.uri)
                    build()
                }
                call.respondRedirect(redirectUrl)
            }
        }
    }
}
a

Aleksei Tirman [JB]

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

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
/login/authorization-callback
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:
Copy code
{
  "access_token": "...",
  "expires_in": 3599,
  "scope": "<https://www.googleapis.com/auth/userinfo.profile>",
  "token_type": "Bearer",
  "id_token": "..."
}
Strava's
Copy code
{
   "token_type":"Bearer",
   "expires_at":1678972747,
   "expires_in":14065,
   "refresh_token":"...",
   "access_token":"...",
   "athlete":{} 
   }
}
Not sure how I can change the expected return type however as this seems omitted in the docs 🤔
90 Views