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
Related
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)
}
}
this is a self answered question about adding access token to request header and refresh the token with refresh token, I was struggling with this topic for a long time, and now I'm writing in post hopefully it could help anyone else in same situations
maybe there would be any other better solutions, but it worked for me in the easiest way
in remote module I'm following this method with the help of Hilt:
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
#Provides
#Singleton
fun providesRetrofit (okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
#Provides
#Singleton
fun providesOkHttpClient(interceptor: AuthInterceptor, authAuthenticator: AuthAuthenticator): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(interceptor)
.authenticator(authAuthenticator)
.build()
}
I send a request to server and receive the access token and refresh token, then I saved them with the power of shared preferences like so:
class TokenManager #Inject constructor(#ApplicationContext context: Context) {
private var prefs: SharedPreferences =
context.getSharedPreferences(PREFS_TOKEN_FILE, Context.MODE_PRIVATE)
fun saveToken(token: UserAuthModel?) {
val editor = prefs.edit()
token?.let {
editor.putString(USER_TOKEN, token.access_token).apply()
editor.putString(USER_REFRESH_TOKEN,token.refresh_token).apply()
editor.putBoolean(IS_LOGGED_IN,true).apply ()
}
}
fun getToken(): String? {
return prefs.getString(USER_TOKEN, null)
}
fun getRefreshToken(): String? {
return prefs.getString(USER_REFRESH_TOKEN, null)
}}
then I use .addInterceptor(interceptor) in order to add header to all requests like this:
class AuthInterceptor #Inject constructor():Interceptor{
#Inject
lateinit var tokenManager: TokenManager
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
val token = tokenManager.getToken()
request.addHeader("Authorization", "Bearer $token")
request.addHeader("Accept","application/json")
return chain.proceed(request.build())
}}
after that you will have access to every method which requires access token as authentication modo, depending on your API instruction your access token will be expired in a specific time ( maybe 24 hours) and you need a new access token which is accessible with help of refresh token that you already have it, and then I add this line to okHttp .authenticator(authAuthenticator)
when your access token expires, the API will send you back a 401 or 403 error code (it will happen in interceptor section), and in that time Authenticator came into play, luckily it's smart enough to recognize this and do the task,
I take care of Authenticator like this:
class AuthAuthenticator #Inject constructor() : Authenticator {
#Inject lateinit var tokenManager: TokenManager
override fun authenticate(route: Route?, response: Response): Request? {
return runBlocking {
val refreshToken=tokenManager.getRefreshToken()
val refreshTokenR:RequestBody= refreshToken?.toRequestBody() ?: "".toRequestBody()
val grantTypeR:RequestBody= "refresh_token".toRequestBody()
//val newAccessToken = authService.safeRefreshTokenFromApi(refreshToken,grantType)
val newAccessToken = getUpdatedToken(refreshTokenR,grantTypeR)
if (!newAccessToken.isSuccessful){
val intent=Intent(context,MainActivity::class.java)
context.startActivity(intent)
}
tokenManager.saveToken(newAccessToken.body()) // save new access_token for next called
newAccessToken.body()?.let {
response.request.newBuilder()
.header("Authorization", "Bearer ${it.access_token}") // just only need to
override "Authorization" header, don't need to override all header since this new request
is create base on old request
.build()
}
} }
private suspend fun getUpdatedToken( refreshToken:RequestBody,grantType:RequestBody):
retrofit2.Response<UserAuthModel> {
val okHttpClient = OkHttpClient.Builder()
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
val service=retrofit.create(AuthService::class.java)
return service.refreshTokenFromApi(refreshToken,grantType)
}}
Authenticator need to make a request, so it needs a retrofit and OkHttp instance( which will run this very Authenticator),in order to break this cycle I created another instance .
two things I have to mention is :
I guess it's ok to use runBlocking because Authenticator itself is running on another thread
and remember in case of kotlin you have to use suspend function in API service to take care of Unable to create call adapter for retrofit2.Response error
at the end, I have to mention that I'm using two different API service like this:
general service:
interface MovieService {
#GET("api/v1/movies/{movie-id}")
suspend fun getSingleMovie(#Path("movie-id") movieId:Int):Response<NetworkMovieModel>}
authentication service:
interface AuthService:MovieService {
#Multipart
#POST("oauth/token")
fun refreshTokenFromApi (#Part("refresh_token") username: RequestBody,
#Part("grant_type") grantType: RequestBody
): Response<UserAuthModel>}
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.
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()) }
)
}
I have a completable method for authentication and I want to retrieve the authenticated user details on login success.
After retrieving them, I want to call an onUserAuthenticated method.
I am doing this with a nested completable (2 levels deep), as I want to sent both the authorization token received on login and the user details to the onUserAuthenticated method.
The problem is that onUserAuthenticated never gets invoked.
class LoginViewModel(val emailAuthentication: EmailAuthentication) : ViewModel() {
val email = MutableLiveData<String>()
val password = MutableLiveData<String>()
fun login() {
emailAuthentication.login(email = email.value!!, password = password.value!!)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Timber.d("User $email logged in")
}, { error ->
Timber.e(error, "Error logging in $email")
})
}
}
class EmailAuthenticationImpl(private val authentication: Authentication,
private val userRepository: UserRepository,
private val authRepository: AuthenticationRepository
) : EmailAuthentication {
override fun register(email: String, password: String): Completable {
return userRepository.register(email, password)
}
override fun login(email: String, password: String): Completable {
// some missing fields validation
return authRepository.login(email, password)
.flatMapCompletable { token ->
userRepository.getCurrentUser()
.flatMapCompletable {
Completable.defer {
// FIXME this never gets invoked
authentication.onUserAuthenticated(AuthType.EMAIL, it, token)
Completable.complete()
}
}
}
}
I tried putting Completable.defer also only before userRepository.getCurrentUser() and both before userRepository.getCurrentUser() and before authentication.onUserAuthenticated(AuthType.EMAIL, it, token), but the code is never reached.
What am I doing wrong?