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
Send a Get request for auth to accounts.spotify.com/authorize
If the user not auth'd -> launch a webpage for the user to sign-in
Spotify sends a callback to a url provided in the initial request.
Recieve a Token from Spotify
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)
}
}
Related
So im trying to request a side with proxies that is protected with cloudflare. The problem is i get 403 forbidden cloduflare error but only when im using proxies without it works. But the proxies are not the problem i tried them with python(requests module) and in my browser there i dont get blocked. My code
suspend fun scrape() {
val client = HttpClient {
followRedirects = true
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
engine {
proxy =
ProxyBuilder.http("http://ProxyIP:proxyPort")
}
defaultRequest {
val credentials = Base64.getEncoder().encodeToString("ProxyUser:ProxyPassword".toByteArray())
header(HttpHeaders.ProxyAuthorization, "Basic $credentials")
}
}
val response = client.get("http://example.com")
val body = response.bodyAsText()
println(body)
println(response.status.hashCode())
Fixxed it
suspend fun scrape() {
val client = HttpClient(Apache) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
engine {
followRedirects = false
customizeClient {
setProxy(HttpHost("hostname", port))
val credentialsProvider = BasicCredentialsProvider()
credentialsProvider .setCredentials(
AuthScope("hostname", port),
UsernamePasswordCredentials("username", "password")
)
setDefaultCredentialsProvider(credentialsProvider )
}
}
}
val response =
client.get("http://example.com") {
}
val body = response.bodyAsText()
println(body)
println(response.status.hashCode())
}
There is a problem with making a request through a proxy server using the CIO engine. I've created an issue to address this problem. As a workaround, please use the OkHttp engine instead of CIO.
Here is how you can use a proxy with the Basic authentication:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.OkHttpClient
import java.net.Proxy
fun main(): Unit = runBlocking {
val proxyAuthenticator = Authenticator { _, response ->
response.request.newBuilder()
.header("Proxy-Authorization", Credentials.basic("<username>", "<password>"))
.build()
}
val client = HttpClient(OkHttp) {
engine {
preconfigured = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, java.net.InetSocketAddress("127.0.0.1", 3128)))
.proxyAuthenticator(proxyAuthenticator)
.build()
}
}
val response = client.get("http://eu.kith.com/products.json")
println(response.status)
}
I try to send the refresh token to the server when the access token expires and receive a new accesss token, but my code does not work properly.
refresh token class
class RefreshToken(): Authenticator {
override fun authenticate(route: Route?, responsee: Response): Request? {
if (responsee.code == 401) {
lateinit var loginRepository: LoginRepository
lateinit var bodyRefresh: BodyRefresh
lateinit var access: StoreAccess //datastore for save token
lateinit var newAccess: String
CoroutineScope(Dispatchers.Main).launch {
access.getUserRefresh().collect {
val refresh = it.toString()
bodyRefresh.refresh = refresh
val response = loginRepository.RefreshAccess(bodyRefresh)
if (response.isSuccessful) {
access.saveUserRefresh(response.body()?.access.toString())
newAccess = response.body()?.access.toString()
}
}
}
return responsee.request.newBuilder().header("Authorization", "Bearer $newAccess.toString()")
.build()
} else {
return responsee.request
}
}
}
api service
#POST("token/refresh/")
suspend fun refreshAcssec(#Body refresh: BodyRefresh): Response<ResponseAcces>
I'm using runBlocking{ } instead of CoroutineScope(Dispatchers.Main).launch.
It's working for me, but I don't know if is the best solution
I guess it's ok, because Authenticator and Interceptor will run on another thread
check this
enter link description here
im trying to implement what said in this article:
https://blog.coinbase.com/okhttp-oauth-token-refreshes-b598f55dd3b2
im working on an android app using kotlin
for a coibase wallet.
I was able to get the authorization code, and then an authorization token with retrofit. i have used the token to get user information and also refresh a token. But when it came to create address i am getting wrong token response even if im using a newly created token by using the refresh token with the correct scope create wallet address
so as a suggestion , mentor on my course asked me to use that article implementation, so that tokens are refreshed automatically with correct headers and all.
so i can't find a way to implement correctly and cant find an example that uses code from the article.
ill share my code tomorrow, hope someone can help with this. Thank you for your time, i appreciate.
This is the code im trying to implement based on the article:
object UserNetwork {
//private val logger = HttpLoggingInterceptor()
// .setLevel(HttpLoggingInterceptor.Level.BODY )
private val accessTokenProvider = AccessTokenProviderImp()
private val accessTokenInterceptor = AccessTokenInterceptor(accessTokenProvider)
val client = OkHttpClient.Builder()
.addNetworkInterceptor(accessTokenInterceptor)
.authenticator(AccessTokenAuthenticator(accessTokenProvider))
.build()
val coinBaseClienApiCalls:CoinBaseClienApiCalls
get(){
return Retrofit.Builder()
.baseUrl("https://api.coinbase.com/")
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(CoinBaseClienApiCalls::class.java)
}
private class UserCallBack(
private val onSuccess:(UserData.Data) -> Unit): Callback<UserData> {
override fun onResponse(call: Call<UserData>, response: Response<UserData>) {
Log.e("ON Response User:"," ${response.body()?.data?.name}")
val newClient = UserData.Data(
name = response.body()?.data?.name?:"",
avatarUrl = response.body()?.data?.avatarUrl?:"",
id = response.body()?.data?.id?:"",
profileBio = response.body()?.data?.profileBio?:"",
profileLocation = response.body()?.data?.profileLocation?:"",
profileUrl = response.body()?.data?.profileUrl ?:"",
resource = response.body()?.data?.resource?:"",
resourcePath = response.body()?.data?.resourcePath?:"",
username = response.body()?.data?.username?:""
)
Log.e("RESPONDED WITH:","Client: ${newClient.name},${newClient.id} ${response.isSuccessful}")
onSuccess(newClient)
}
override fun onFailure(call: Call<UserData>, t: Throwable) {
Log.e("On Failure Address:","$t")
}
}
fun getUser (onSuccess: (UserData.Data) -> Unit){
var token = if(Repository.accessToken != ""){
Repository.accessToken
}else{
""
}
if(token != ""){
coinBaseClienApiCalls.getUser("Bearer $token").enqueue(UserCallBack(onSuccess)) //getUser(token).enqueue(AddressCallBack(onSuccess))
}else{
Log.e("ACCESS TOKEN IN REPOSITORY","${Repository.accessToken}")
}
}
}
class AccessTokenInterceptor(
private val tokenProvider: AccessTokenProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenProvider.token()
return if (token == null) {
chain.proceed(chain.request())
} else {
val authenticatedRequest = chain.request()
.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
chain.proceed(authenticatedRequest)
}
}
}
interface AccessTokenProvider {
/**
* Returns an access token. In the event that you don't have a token return null.
*/
fun token(): String?
/**
* Refreshes the token and returns it. This call should be made synchronously.
* In the event that the token could not be refreshed return null.
*/
fun refreshToken(): String?
}
class AccessTokenAuthenticator(
private val tokenProvider: AccessTokenProvider
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
// We need to have a token in order to refresh it.
val token = tokenProvider.token() ?: return null
synchronized(this) {
val newToken = tokenProvider.token()
Log.e("NEW TOKEN AUTHENTICATOR","$newToken")
// Check if the request made was previously made as an authenticated request.
if (response.request.header("Authorization") != null) {
// If the token has changed since the request was made, use the new token.
if (newToken != token) {
Log.e("Testing Authenticator1","Testing1")
return response.request
.newBuilder()
.removeHeader("Authorization")
.addHeader("Authorization", "Bearer $newToken")
.build()
}
val updatedToken = tokenProvider.refreshToken() ?: return null
Log.e("Testing Authenticator2","Testing2")
// Retry the request with the new token.
return response.request
.newBuilder()
.removeHeader("Authorization")
.addHeader("Authorization", "Bearer $updatedToken")
.build()
}
}
return null
}
}
class AccessTokenProviderImp():AccessTokenProvider {
var token = Repository.accessToken
override fun token(): String? {
return token
}
override fun refreshToken(): String? {
return token
}
}
I fixed the way the AccessTokenProviderImp() requested the existing token or the newly refreshed token, remove the check if (newToken != token) from Authenticator. Works great.
So I am creating a distributed Key-Value datastore and have a broker to manage/query.
Here is my client:
val client = HttpClient(CIO) {
install(WebSockets)
}
runBlocking {
client.ws(
method = HttpMethod.Get,
host = ip,
port = port,
path = "/thepath"
) {
...
}
}
client.close()
So far I can connect only one server to the client (Obviously by the code above).
What I tried is to create an array of all the available servers and randomly pick one and work with the broker (client). But this works only for the connected server and the others should wait until the connection is closed.
val clients: Array<HttpClient?> = arrayOfNulls(replicationFactor)
for (i in 0 until replicationFactor) {
clients[i] = HttpClient(CIO) {
install(WebSockets)
}
}
runBlocking {
clients[0]?.ws(
method = HttpMethod.Get,
host = "some ip",
port = the_port,
path = "/thepath"
) {
....
}
...
...
}
Any ideas of how to tackle this problem? Maybe I can keep the connection with each server on a separate thread.
You can create any number of HTTP clients and connect them to a server concurrently. Here is an example:
suspend fun main() {
val clients = (0 until 3).map {
HttpClient(CIO) {
install(WebSockets)
}
}
val connections = coroutineScope {
clients.mapIndexed { index, client ->
async {
client.ws("wss://echo.websocket.org") {
outgoing.send(Frame.Text("Hello server"))
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
println("[$index] Server replied ${frame.readText()}")
}
}
}
}
}.toTypedArray()
}
awaitAll(*connections)
}
I need to make a sync call to reauthenticate the user and get a new token, but I haven't found a way that works. The code below blocks the thread and it is never unblocked, ie. I have an infinite loop
class ApolloAuthenticator(private val authenticated: Boolean) : Authenticator {
#Throws(IOException::class)
override fun authenticate(route: Route, response: Response): Request? {
// Refresh your access_token using a synchronous api request
if (response.request().header(HEADER_KEY_APOLLO_AUTHORIZATION) != null) {
return null //if you've tried to authorize and failed, give up
}
synchronized(this) {
refreshTokenSync() // This is blocked and never unblocked
val newToken = getApolloTokenFromSharedPreference()
return response.request().newBuilder()
.header(HEADER_KEY_APOLLO_AUTHORIZATION, newToken)
.build()
}
private fun refreshTokenSync(): EmptyResult {
//Refresh token, synchronously
val repository = Injection.provideSignInRepository()
return repository
.signInGraphQL()
.toBlocking()
.first()
}
fun signInGraphQL() : Observable<EmptyResult> =
sharedPreferencesDataSource.identifier
.flatMap { result -> graphqlAuthenticationDataSource.getAuth(result) }
.flatMap { result -> sharedPreferencesDataSource.saveApolloToken(result) }
.onErrorReturn { EmptyResult() }
}
---------- Use of it
val apollAuthenticator = ApolloAuthenticator(authenticated)
val okHttpBuilder =
OkHttpClient.Builder()
.authenticator(apollAuthenticator)
I haven't found a way to make a sync call using RxJava, but I can make it by using kotlin coutorine runBlocking, which will block the thread until the request is finished:
synchronized(this) {
runBlocking {
val subscription = ApolloReauthenticator.signInGraphQl() // await until it's finished
subscription.unsubscribe()
}
}
fun signInGraphQl(): Subscription {
return repository.refreshToken()
.subscribe(
{ Observable.just(EmptyResult()) },
{ Observable.just(EmptyResult()) }
)
}