I have Ktor Jwt authentication setup with Firebase Auth and working except in my unit testing. Getting TRACE io.ktor.auth.jwt - Failed to get JWK: Failed to get key with kid null
I'm wondering if FirebaseAuth.getInstance().createCustomToken(UUID.randomUUID().toString()) is not working.
Here is one of my unit tests:
test("when register for game and userId is not found then NotFound is returned") {
coEvery { userRepositoryMock.getUser(any()) } returns null
withServer {
val req = handleRequest(HttpMethod.Get, "/api/game/1/2") {
addJwtHeader()
}
req.response.status() shouldBe HttpStatusCode.NotFound
req.response.content shouldBe "Could not find user."
}
}
private fun TestApplicationRequest.addJwtHeader() = addHeader("Authorization", "Bearer ${getToken()}")
private fun getToken() = FirebaseAuth.getInstance().createCustomToken(UUID.randomUUID().toString())
private fun withServer(block: TestApplicationEngine.() -> Unit) {
withTestApplication({ module() }, block)
}
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)
}
}
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.
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?