diff --git a/app/src/main/java/org/fossasia/openevent/general/StartupViewModel.kt b/app/src/main/java/org/fossasia/openevent/general/StartupViewModel.kt index ab955270b..a30cc3401 100644 --- a/app/src/main/java/org/fossasia/openevent/general/StartupViewModel.kt +++ b/app/src/main/java/org/fossasia/openevent/general/StartupViewModel.kt @@ -20,6 +20,10 @@ import org.fossasia.openevent.general.utils.extensions.withDefaultSchedulers import retrofit2.HttpException import timber.log.Timber +const val HEADER_TYPE = "headerType" +const val JWT_ACCESS_TOKEN = "jwtAccessToken" +const val JWT_REFRESH_TOKEN = "jwtRefreshToken" + class StartupViewModel( private val preference: Preference, private val resource: Resource, @@ -116,4 +120,23 @@ class StartupViewModel( Timber.e(it, "Error in fetching settings form API") }) } + + fun checkAccessToken() { + if (!authHolder.isLoggedIn() || authHolder.isTokenValid()) + return + preference.putString(HEADER_TYPE, JWT_REFRESH_TOKEN) + compositeDisposable += authService.refreshToken() + .withDefaultSchedulers() + .doFinally { + preference.putString(HEADER_TYPE, JWT_ACCESS_TOKEN) + }.subscribe({ + authHolder.accessToken = it.accessToken + }, { + if (it is HttpException) { + if (it.code() == HttpErrors.UNAUTHORIZED) + logoutAndRefresh() + } + Timber.e(it, "Error refreshing token, logged out.") + }) + } } diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/AuthApi.kt b/app/src/main/java/org/fossasia/openevent/general/auth/AuthApi.kt index beea5821d..301fb6b1f 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/AuthApi.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/AuthApi.kt @@ -16,7 +16,7 @@ import retrofit2.http.DELETE interface AuthApi { - @POST("../auth/session") + @POST("auth/login") fun login(@Body login: Login): Single @GET("users/{id}") @@ -51,4 +51,7 @@ interface AuthApi { @DELETE("users/{id}") fun deleteAccount(@Path("id") userId: Long): Completable + + @POST("/auth/token/refresh") + fun refreshToken(): Single } diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/AuthHolder.kt b/app/src/main/java/org/fossasia/openevent/general/auth/AuthHolder.kt index a474ae230..76428320b 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/AuthHolder.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/AuthHolder.kt @@ -3,30 +3,52 @@ 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") + throw IllegalStateException("Cannot set expired accessToken") field = value - preference.putString(TOKEN_KEY, value) + preference.putString(ACCESS_TOKEN, value) } - fun getAuthorization(): String? { + var refreshToken: String? = null + get() { + return preference.getString(REFRESH_TOKEN) + } + set(value) { + if (value != null && JWTUtils.isExpired(value)) + throw IllegalStateException("Cannot set expired refreshToken") + 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 $token" + return "JWT $refreshToken" + } + + fun isTokenValid(): Boolean { + return accessToken != null && !JWTUtils.isExpired(accessToken) } fun isLoggedIn(): Boolean { - if (token == null || JWTUtils.isExpired(token)) { - token = null + if (accessToken == null || JWTUtils.isExpired(accessToken)) { + accessToken = null return false } @@ -34,6 +56,6 @@ class AuthHolder(private val preference: Preference) { } fun getId(): Long { - return if (!isLoggedIn()) -1 else JWTUtils.getIdentity(token) + return if (!isLoggedIn()) -1 else JWTUtils.getIdentity(accessToken) } } diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/AuthService.kt b/app/src/main/java/org/fossasia/openevent/general/auth/AuthService.kt index aee7017c3..84452d1c0 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/AuthService.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/AuthService.kt @@ -9,7 +9,6 @@ import org.fossasia.openevent.general.auth.change.Password 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 org.fossasia.openevent.general.event.EventApi import org.fossasia.openevent.general.event.EventDao import org.fossasia.openevent.general.order.OrderDao import timber.log.Timber @@ -20,8 +19,7 @@ class AuthService( private val userDao: UserDao, private val orderDao: OrderDao, private val attendeeDao: AttendeeDao, - private val eventDao: EventDao, - private val eventApi: EventApi + private val eventDao: EventDao ) { fun login(username: String, password: String): Single { if (username.isEmpty() || password.isEmpty()) @@ -29,7 +27,8 @@ class AuthService( return authApi.login(Login(username, password)) .map { - authHolder.token = it.accessToken + authHolder.accessToken = it.accessToken + authHolder.refreshToken = it.refreshToken it } } @@ -66,7 +65,7 @@ class AuthService( fun logout(): Completable { return Completable.fromAction { - authHolder.token = null + authHolder.accessToken = null userDao.deleteUser(authHolder.getId()) orderDao.deleteAllOrders() attendeeDao.deleteAllAttendees() @@ -122,4 +121,8 @@ class AuthService( fun resetPassword(requestPasswordReset: RequestPasswordReset): Single { return authApi.resetPassword(requestPasswordReset) } + + fun refreshToken(): Single { + return authApi.refreshToken() + } } diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/Login.kt b/app/src/main/java/org/fossasia/openevent/general/auth/Login.kt index 26dc2d063..6b7d2da19 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/Login.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/Login.kt @@ -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 +) diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/LoginResponse.kt b/app/src/main/java/org/fossasia/openevent/general/auth/LoginResponse.kt index ff0047302..c5030fdea 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/LoginResponse.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/LoginResponse.kt @@ -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 +) diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/RequestAuthenticator.kt b/app/src/main/java/org/fossasia/openevent/general/auth/RequestAuthenticator.kt index 4e0fb27b2..94d5cdc0f 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/RequestAuthenticator.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/RequestAuthenticator.kt @@ -2,11 +2,23 @@ package org.fossasia.openevent.general.auth import okhttp3.Interceptor import okhttp3.Response +import org.fossasia.openevent.general.HEADER_TYPE +import org.fossasia.openevent.general.JWT_ACCESS_TOKEN +import org.fossasia.openevent.general.JWT_REFRESH_TOKEN +import org.fossasia.openevent.general.data.Preference -class RequestAuthenticator(private val authHolder: AuthHolder) : Interceptor { +class RequestAuthenticator( + private val authHolder: AuthHolder, + private val preference: Preference +) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { - val authorization = authHolder.getAuthorization() + val headerType = preference.getString(HEADER_TYPE, JWT_ACCESS_TOKEN) + val authorization = + if (headerType == JWT_REFRESH_TOKEN) + authHolder.getRefreshAuthorization() + else + authHolder.getAccessAuthorization() val original = chain.request() return if (authorization != null) { val request = original.newBuilder() diff --git a/app/src/main/java/org/fossasia/openevent/general/di/Modules.kt b/app/src/main/java/org/fossasia/openevent/general/di/Modules.kt index 3436123ac..cd3378e88 100644 --- a/app/src/main/java/org/fossasia/openevent/general/di/Modules.kt +++ b/app/src/main/java/org/fossasia/openevent/general/di/Modules.kt @@ -218,7 +218,7 @@ val apiModule = module { } factory { AuthHolder(get()) } - factory { AuthService(get(), get(), get(), get(), get(), get(), get()) } + factory { AuthService(get(), get(), get(), get(), get(), get()) } factory { EventService(get(), get(), get(), get(), get(), get(), get(), get(), get()) } factory { SpeakerService(get(), get(), get()) } @@ -296,7 +296,7 @@ val networkModule = module { .connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS) .readTimeout(readTimeout.toLong(), TimeUnit.SECONDS) .addInterceptor(HostSelectionInterceptor(get())) - .addInterceptor(RequestAuthenticator(get())) + .addInterceptor(RequestAuthenticator(get(), get())) .addNetworkInterceptor(StethoInterceptor()) if (BuildConfig.DEBUG) { diff --git a/app/src/main/java/org/fossasia/openevent/general/event/EventsFragment.kt b/app/src/main/java/org/fossasia/openevent/general/event/EventsFragment.kt index ad8ba0774..92249c3e5 100644 --- a/app/src/main/java/org/fossasia/openevent/general/event/EventsFragment.kt +++ b/app/src/main/java/org/fossasia/openevent/general/event/EventsFragment.kt @@ -104,6 +104,7 @@ class EventsFragment : Fragment(), BottomIconDoubleClick { rootView.eventsRecycler.adapter = eventsListAdapter rootView.eventsRecycler.isNestedScrollingEnabled = false + startupViewModel.checkAccessToken() startupViewModel.syncNotifications() startupViewModel.fetchSettings() handleNotificationDotVisibility( diff --git a/app/src/main/java/org/fossasia/openevent/general/order/OrderDetailsViewModel.kt b/app/src/main/java/org/fossasia/openevent/general/order/OrderDetailsViewModel.kt index f7f2176cb..926576ec9 100644 --- a/app/src/main/java/org/fossasia/openevent/general/order/OrderDetailsViewModel.kt +++ b/app/src/main/java/org/fossasia/openevent/general/order/OrderDetailsViewModel.kt @@ -49,7 +49,7 @@ class OrderDetailsViewModel( }) } - fun getToken() = authHolder.getAuthorization().nullToEmpty() + fun getToken() = authHolder.getAccessAuthorization().nullToEmpty() fun loadAttendeeDetails(orderId: Long) { if (orderId == -1L) return diff --git a/app/src/main/java/org/fossasia/openevent/general/utils/JWTUtils.java b/app/src/main/java/org/fossasia/openevent/general/utils/JWTUtils.java index 07c6a6dc5..1116de15b 100644 --- a/app/src/main/java/org/fossasia/openevent/general/utils/JWTUtils.java +++ b/app/src/main/java/org/fossasia/openevent/general/utils/JWTUtils.java @@ -7,7 +7,7 @@ public class JWTUtils { - public static SparseArrayCompat decode(String token) { + private static SparseArrayCompat decode(String token) { SparseArrayCompat decoded = new SparseArrayCompat<>(2); String[] split = token.split("\\."); @@ -17,7 +17,7 @@ public static SparseArrayCompat decode(String token) { return decoded; } - public static long getExpiry(String token) throws JSONException { + private static long getExpiry(String token) throws JSONException { SparseArrayCompat decoded = decode(token); // We are using JSONObject instead of GSON as it takes about 5 ms instead of 150 ms taken by GSON