Skip to content

Commit 52a6cf2

Browse files
feat: Implement JWT refresh token for authorization (fossasia#2230)
1 parent f0d8f01 commit 52a6cf2

File tree

8 files changed

+124
-18
lines changed

8 files changed

+124
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.fossasia.openevent.general.auth
2+
3+
import okhttp3.Authenticator
4+
import okhttp3.Request
5+
import okhttp3.Response
6+
import okhttp3.Route
7+
import org.koin.core.KoinComponent
8+
import org.koin.core.inject
9+
10+
class TokenAuthenticator : Authenticator, KoinComponent {
11+
12+
val tokenService: RefreshTokenService by inject()
13+
val authHolder: AuthHolder by inject()
14+
15+
/**
16+
* Authenticator for when the authToken need to be refresh and updated
17+
* everytime we get a 401 error code
18+
*/
19+
20+
override fun authenticate(route: Route?, response: Response): Request? {
21+
22+
val loginResponse = tokenService.refreshToken()
23+
24+
return if (loginResponse.isSuccessful) {
25+
/**
26+
* Replace the existing tokens with the new tokens
27+
**/
28+
loginResponse.body()?.let {
29+
authHolder.accessToken = it.accessToken
30+
authHolder.refreshToken = it.refreshToken
31+
32+
val newToken = "JWT ${it.accessToken}"
33+
34+
response.request.newBuilder()
35+
.addHeader("Authorization", newToken)
36+
.build()
37+
}
38+
} else {
39+
authHolder.accessToken = null
40+
authHolder.refreshToken = null
41+
response.request
42+
}
43+
}
44+
}

app/src/main/java/org/fossasia/openevent/general/auth/AuthApi.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@ import org.fossasia.openevent.general.auth.change.ChangeRequestTokenResponse
77
import org.fossasia.openevent.general.auth.forgot.Email
88
import org.fossasia.openevent.general.auth.forgot.RequestToken
99
import org.fossasia.openevent.general.auth.forgot.RequestTokenResponse
10+
import retrofit2.Response
1011
import retrofit2.http.Body
12+
import retrofit2.http.DELETE
1113
import retrofit2.http.GET
1214
import retrofit2.http.PATCH
1315
import retrofit2.http.POST
1416
import retrofit2.http.Path
15-
import retrofit2.http.DELETE
1617

1718
interface AuthApi {
1819

19-
@POST("../auth/session")
20+
@POST("auth/login")
2021
fun login(@Body login: Login): Single<LoginResponse>
2122

23+
@POST("/auth/token/refresh")
24+
fun refreshToken(): Response<LoginResponse>
25+
2226
@GET("users/{id}")
2327
fun getProfile(@Path("id") id: Long): Single<User>
2428

app/src/main/java/org/fossasia/openevent/general/auth/AuthHolder.kt

+31-10
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,58 @@ package org.fossasia.openevent.general.auth
33
import org.fossasia.openevent.general.data.Preference
44
import org.fossasia.openevent.general.utils.JWTUtils
55

6-
private const val TOKEN_KEY = "TOKEN"
6+
private const val ACCESS_TOKEN = "accessToken"
7+
private const val REFRESH_TOKEN = "refreshToken"
78

89
class AuthHolder(private val preference: Preference) {
910

10-
var token: String? = null
11+
var accessToken: String? = null
1112
get() {
12-
return preference.getString(TOKEN_KEY)
13+
return preference.getString(ACCESS_TOKEN)
1314
}
1415
set(value) {
15-
if (value != null && JWTUtils.isExpired(value))
16-
throw IllegalStateException("Cannot set expired token")
16+
check(!(value != null && JWTUtils.isExpired(value))) { "Cannot set expired token" }
1717
field = value
18-
preference.putString(TOKEN_KEY, value)
18+
preference.putString(ACCESS_TOKEN, value)
1919
}
20+
var refreshToken: String? = null
21+
get() {
22+
return preference.getString(REFRESH_TOKEN)
23+
}
24+
set(value) {
25+
check(!(value != null && JWTUtils.isExpired(value))) { "Cannot set expired token" }
26+
field = value
27+
preference.putString(REFRESH_TOKEN, value)
28+
}
29+
30+
fun getAccessAuthorization(): String? {
31+
if (!isLoggedIn())
32+
return null
33+
return "JWT $accessToken"
34+
}
35+
36+
fun getRefreshAuthorization(): String? {
37+
if (!isLoggedIn())
38+
return null
39+
return "JWT $refreshToken"
40+
}
2041

2142
fun getAuthorization(): String? {
2243
if (!isLoggedIn())
2344
return null
24-
return "JWT $token"
45+
return "JWT $accessToken"
2546
}
2647

2748
fun isLoggedIn(): Boolean {
28-
if (token == null || JWTUtils.isExpired(token)) {
29-
token = null
49+
if (accessToken == null || JWTUtils.isExpired(accessToken)) {
50+
accessToken = null
3051
return false
3152
}
3253

3354
return true
3455
}
3556

3657
fun getId(): Long {
37-
return if (!isLoggedIn()) -1 else JWTUtils.getIdentity(token)
58+
return if (!isLoggedIn()) -1 else JWTUtils.getIdentity(accessToken)
3859
}
3960
}

app/src/main/java/org/fossasia/openevent/general/auth/AuthService.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ class AuthService(
2424
private val eventApi: EventApi
2525
) {
2626
fun login(username: String, password: String): Single<LoginResponse> {
27-
if (username.isEmpty() || password.isEmpty())
28-
throw IllegalArgumentException("Username or password cannot be empty")
27+
require(!(username.isEmpty() || password.isEmpty())) { "Username or password cannot be empty" }
2928

3029
return authApi.login(Login(username, password))
3130
.map {
32-
authHolder.token = it.accessToken
31+
authHolder.accessToken = it.accessToken
32+
authHolder.refreshToken = it.refreshToken
3333
it
3434
}
3535
}
@@ -66,7 +66,8 @@ class AuthService(
6666

6767
fun logout(): Completable {
6868
return Completable.fromAction {
69-
authHolder.token = null
69+
authHolder.accessToken = null
70+
authHolder.refreshToken = null
7071
userDao.deleteUser(authHolder.getId())
7172
orderDao.deleteAllOrders()
7273
attendeeDao.deleteAllAttendees()
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
11
package org.fossasia.openevent.general.auth
22

3-
data class Login(val email: String, val password: String)
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategy
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming
5+
6+
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
7+
data class Login(
8+
val email: String,
9+
val password: String,
10+
val rememberMe: Boolean = true,
11+
val includeInResponse: Boolean = true
12+
)

app/src/main/java/org/fossasia/openevent/general/auth/LoginResponse.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy
44
import com.fasterxml.jackson.databind.annotation.JsonNaming
55

66
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
7-
data class LoginResponse(val accessToken: String)
7+
data class LoginResponse(
8+
val accessToken: String,
9+
val refreshToken: String
10+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.fossasia.openevent.general.auth
2+
3+
import org.fossasia.openevent.general.BuildConfig
4+
import retrofit2.Response
5+
import retrofit2.Retrofit
6+
import retrofit2.converter.gson.GsonConverterFactory
7+
8+
class RefreshTokenService {
9+
10+
private val retrofit = Retrofit.Builder()
11+
.addConverterFactory(GsonConverterFactory.create())
12+
.baseUrl(BuildConfig.DEFAULT_BASE_URL)
13+
.build()
14+
15+
private val authApi: AuthApi = retrofit.create(AuthApi::class.java)
16+
17+
fun refreshToken(): Response<LoginResponse> {
18+
return authApi.refreshToken()
19+
}
20+
}

app/src/main/java/org/fossasia/openevent/general/di/Modules.kt

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import org.fossasia.openevent.general.auth.SignUp
3030
import org.fossasia.openevent.general.auth.SignUpViewModel
3131
import org.fossasia.openevent.general.auth.User
3232
import org.fossasia.openevent.general.auth.AuthViewModel
33+
import org.fossasia.openevent.general.auth.RefreshTokenService
3334
import org.fossasia.openevent.general.data.Network
3435
import org.fossasia.openevent.general.data.Preference
3536
import org.fossasia.openevent.general.event.Event
@@ -71,6 +72,7 @@ import org.fossasia.openevent.general.search.location.LocationService
7172
import org.fossasia.openevent.general.search.type.SearchTypeViewModel
7273
import org.fossasia.openevent.general.search.location.LocationServiceImpl
7374
import org.fossasia.openevent.general.auth.SmartAuthViewModel
75+
import org.fossasia.openevent.general.auth.TokenAuthenticator
7476
import org.fossasia.openevent.general.connectivity.MutableConnectionLiveData
7577
import org.fossasia.openevent.general.discount.DiscountApi
7678
import org.fossasia.openevent.general.discount.DiscountCode
@@ -231,6 +233,7 @@ val apiModule = module {
231233
factory { FeedbackService(get(), get()) }
232234
factory { SettingsService(get(), get()) }
233235
factory { TaxService(get(), get()) }
236+
factory { RefreshTokenService() }
234237
}
235238

236239
val viewModelModule = module {
@@ -296,6 +299,7 @@ val networkModule = module {
296299
.readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
297300
.addInterceptor(HostSelectionInterceptor(get()))
298301
.addInterceptor(RequestAuthenticator(get()))
302+
.authenticator(TokenAuthenticator())
299303
.addNetworkInterceptor(StethoInterceptor())
300304

301305
if (BuildConfig.DEBUG) {

0 commit comments

Comments
 (0)