Skip to content

feat: Implement JWT refresh token for authorization (#2230) #2349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import org.fossasia.openevent.general.auth.change.ChangeRequestTokenResponse
import org.fossasia.openevent.general.auth.forgot.Email
import org.fossasia.openevent.general.auth.forgot.RequestToken
import org.fossasia.openevent.general.auth.forgot.RequestTokenResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.DELETE

interface AuthApi {

@POST("../auth/session")
@POST("auth/login")
fun login(@Body login: Login): Single<LoginResponse>

@POST("/auth/token/refresh")
fun refreshToken(): Response<LoginResponse>

@GET("users/{id}")
fun getProfile(@Path("id") id: Long): Single<User>

Expand Down
41 changes: 31 additions & 10 deletions app/src/main/java/org/fossasia/openevent/general/auth/AuthHolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,58 @@ package org.fossasia.openevent.general.auth
import org.fossasia.openevent.general.data.Preference
import org.fossasia.openevent.general.utils.JWTUtils

private const val TOKEN_KEY = "TOKEN"
private const val ACCESS_TOKEN = "accessToken"
private const val REFRESH_TOKEN = "refreshToken"

class AuthHolder(private val preference: Preference) {

var token: String? = null
var accessToken: String? = null
get() {
return preference.getString(TOKEN_KEY)
return preference.getString(ACCESS_TOKEN)
}
set(value) {
if (value != null && JWTUtils.isExpired(value))
throw IllegalStateException("Cannot set expired token")
check(!(value != null && JWTUtils.isExpired(value))) { "Cannot set expired token" }
field = value
preference.putString(TOKEN_KEY, value)
preference.putString(ACCESS_TOKEN, value)
}
var refreshToken: String? = null
get() {
return preference.getString(REFRESH_TOKEN)
}
set(value) {
check(!(value != null && JWTUtils.isExpired(value))) { "Cannot set expired token" }
field = value
preference.putString(REFRESH_TOKEN, value)
}

fun getAccessAuthorization(): String? {
if (!isLoggedIn())
return null
return "JWT $accessToken"
}

fun getRefreshAuthorization(): String? {
if (!isLoggedIn())
return null
return "JWT $refreshToken"
}

fun getAuthorization(): String? {
if (!isLoggedIn())
return null
return "JWT $token"
return "JWT $accessToken"
}

fun isLoggedIn(): Boolean {
if (token == null || JWTUtils.isExpired(token)) {
token = null
if (accessToken == null || JWTUtils.isExpired(accessToken)) {
accessToken = null
return false
}

return true
}

fun getId(): Long {
return if (!isLoggedIn()) -1 else JWTUtils.getIdentity(token)
return if (!isLoggedIn()) -1 else JWTUtils.getIdentity(accessToken)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ class AuthService(
private val eventApi: EventApi
) {
fun login(username: String, password: String): Single<LoginResponse> {
if (username.isEmpty() || password.isEmpty())
throw IllegalArgumentException("Username or password cannot be empty")
require(!(username.isEmpty() || password.isEmpty())) { "Username or password cannot be empty" }

return authApi.login(Login(username, password))
.map {
authHolder.token = it.accessToken
authHolder.accessToken = it.accessToken
authHolder.refreshToken = it.refreshToken
it
}
}
Expand Down Expand Up @@ -66,7 +66,8 @@ class AuthService(

fun logout(): Completable {
return Completable.fromAction {
authHolder.token = null
authHolder.accessToken = null
authHolder.refreshToken = null
userDao.deleteUser(authHolder.getId())
orderDao.deleteAllOrders()
attendeeDao.deleteAllAttendees()
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/java/org/fossasia/openevent/general/auth/Login.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
package org.fossasia.openevent.general.auth

data class Login(val email: String, val password: String)
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming

@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
data class Login(
val email: String,
val password: String,
val rememberMe: Boolean = true,
val includeInResponse: Boolean = true
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming

@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class LoginResponse(val accessToken: String)
data class LoginResponse(
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.fossasia.openevent.general.auth

import org.fossasia.openevent.general.BuildConfig
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class RefreshTokenService {

private val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BuildConfig.DEFAULT_BASE_URL)
.build()

private val authApi: AuthApi = retrofit.create(AuthApi::class.java)

fun refreshToken(): Response<LoginResponse> {
return authApi.refreshToken()
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it like other classes with DI, and manually inject

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manually inject this class into the TokenAutheticator?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamareebjamal take a look and i also couldn't figure out a method to logout user in case if the refresh token is expired

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete the tokens

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes right 😅

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.fossasia.openevent.general.auth

import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import org.koin.core.KoinComponent
import org.koin.core.inject

class TokenAuthenticator : Authenticator, KoinComponent {

val tokenService: RefreshTokenService by inject()
val authHolder: AuthHolder by inject()

/**
* Authenticator for when the authToken need to be refresh and updated
* everytime we get a 401 error code
*/

override fun authenticate(route: Route?, response: Response): Request? {

val loginResponse = tokenService.refreshToken()

return if (loginResponse.isSuccessful) {
/**
* Replace the existing tokens with the new tokens
**/
loginResponse.body()?.let {
authHolder.accessToken = it.accessToken
authHolder.refreshToken = it.refreshToken

val newToken = "JWT ${it.accessToken}"

response.request.newBuilder()
.addHeader("Authorization", newToken)
.build()
}
} else {
authHolder.accessToken = null
authHolder.refreshToken = null
response.request
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.fossasia.openevent.general.auth.SignUp
import org.fossasia.openevent.general.auth.SignUpViewModel
import org.fossasia.openevent.general.auth.User
import org.fossasia.openevent.general.auth.AuthViewModel
import org.fossasia.openevent.general.auth.RefreshTokenService
import org.fossasia.openevent.general.data.Network
import org.fossasia.openevent.general.data.Preference
import org.fossasia.openevent.general.event.Event
Expand Down Expand Up @@ -71,6 +72,7 @@ import org.fossasia.openevent.general.search.location.LocationService
import org.fossasia.openevent.general.search.type.SearchTypeViewModel
import org.fossasia.openevent.general.search.location.LocationServiceImpl
import org.fossasia.openevent.general.auth.SmartAuthViewModel
import org.fossasia.openevent.general.auth.TokenAuthenticator
import org.fossasia.openevent.general.connectivity.MutableConnectionLiveData
import org.fossasia.openevent.general.discount.DiscountApi
import org.fossasia.openevent.general.discount.DiscountCode
Expand Down Expand Up @@ -231,6 +233,7 @@ val apiModule = module {
factory { FeedbackService(get(), get()) }
factory { SettingsService(get(), get()) }
factory { TaxService(get(), get()) }
factory { RefreshTokenService() }
}

val viewModelModule = module {
Expand Down Expand Up @@ -296,6 +299,7 @@ val networkModule = module {
.readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
.addInterceptor(HostSelectionInterceptor(get()))
.addInterceptor(RequestAuthenticator(get()))
.authenticator(TokenAuthenticator())
.addNetworkInterceptor(StethoInterceptor())

if (BuildConfig.DEBUG) {
Expand Down