I'm building my first desktop application using JetBrains Compose & Ktor. I want to connect to the Spotify API using the Spotify Auth PCKE extension. The flow should be
I'm facing an issue when launching the Webpage for user sign-in and getting the response on the URL I've provided. I'm launching the webpage using the desktop's default browse and opening a websocket that should be listening to "http://localhost:8888/callback)." I noticed when I interact with the launched webpage, the url shows the response that Spotify sent back ("http://localhost:8888/callback?error=access_denied&state=initial" but my websocket code is never called. Is this an issue with how I'm launching the browser or the websocket... or am I going about this wrong in general?
class SpotifyClient {
private val client: HttpClient = HttpClient(CIO) {
followRedirects = true
install(WebSockets) {
contentConverter = KotlinxWebsocketSerializationConverter(Json)
}
handleSpotifyAccountAuthResponse()
}
private val clientId = "<Removed for Post>"
suspend fun authorizeSpotifyClient(): HttpResponse {
val withContext = withContext(Dispatchers.IO) {
val response: HttpResponse =
client.get(NetworkConstants.BASE_URL_SPOTIFY_ACCOUNTS + NetworkConstants.PATH_SPOTIFY_AUTH) {
header("Location", NetworkConstants.BASE_URL_SPOTIFY_AUTH_REDIRECT_URI)
parameter("client_id", clientId)
parameter("response_type", "code")
parameter("redirect_uri", NetworkConstants.BASE_URL_SPOTIFY_AUTH_REDIRECT_URI)
parameter("state", ClientStates.INITIAL.value)
parameter("show_dialog", false)
parameter("code_challenge_method", PCKE_CODE_CHALLENGE_METHOD)
parameter("code_challenge", generatePCKECodeChallenge())
parameter(
"scope",
NetworkUtils.getSpotifyScopes(
ImmutableList.of(
SpotifyScopes.USER_READ_PLAYBACK_STATE,
SpotifyScopes.USER_READ_CURRENTLY_PLAYING
)
)
)
}
println("Auth Spotify API Response $response")
println("Auth Spotify API Response Code ${response.status}")
client.close()
return@withContext response
}
accountAuthRedirectWebsocket()
return withContext
}
private fun HttpClientConfig<CIOEngineConfig>.handleSpotifyAccountAuthResponse() {
HttpResponseValidator {
validateResponse { response ->
handleValidAccountResponse(response)
}
handleResponseExceptionWithRequest { exception, request ->
}
}
}
private fun handleValidAccountResponse(response: HttpResponse) {
if (response.status.value == 200) { // success
val responseUrl = response.call.request.url
if (responseUrl.toString().contains("continue")) {
println("Needs a redirect, ${responseUrl.toString()}")
openWebpage(responseUrl.toURI())
}
}
}
private fun openWebpage(uri: URI?): Boolean {
val desktop = if (Desktop.isDesktopSupported()) Desktop.getDesktop() else null
if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
try {
desktop.browse(uri)
return true
} catch (e: Exception) {
e.printStackTrace()
}
}
return false
}
private suspend fun accountAuthRedirectWebsocket(){
client.webSocket(method = HttpMethod.Get, host = "localhost", port = 8888, path = "/customer/1") {
println("Got a response in the socket")
}
}
To receive the callback I needed to implement a local server like below. Using the WebSocket was the wrong approach in this case and didn't provide the functionality I was expecting.
object SpotifyServer {
suspend fun initServer() {
embeddedServer(CIO,
host = "127.0.0.1",
port = 8080,
configure = {}) {
routing {
get("/callback") {
call.respondText("Got a callback response")
}
}
}.start(wait = true)
}
}