Skip to content

Commit

Permalink
Feature/add support for jwt tokens (#71)
Browse files Browse the repository at this point in the history
* Added support of JWT tokens.
The type of access token can be changed in the config.yaml file.
  • Loading branch information
volodymyr-chekyrta authored Oct 23, 2023
1 parent e23a91c commit 99085e0
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.openedx.app.data.networking

import org.openedx.core.ApiConstants
import okhttp3.Interceptor
import okhttp3.Response
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.BuildConfig.ACCESS_TOKEN_TYPE

class HeadersInterceptor(private val preferencesManager: CorePreferences) : Interceptor {

Expand All @@ -14,7 +14,7 @@ class HeadersInterceptor(private val preferencesManager: CorePreferences) : Inte
val token = preferencesManager.accessToken

if (token.isNotEmpty()) {
addHeader("Authorization", "${ApiConstants.TOKEN_TYPE_BEARER} $token")
addHeader("Authorization", "$ACCESS_TOKEN_TYPE $token")
}

addHeader("Accept", "application/json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ package org.openedx.app.data.networking

import android.util.Log
import com.google.gson.Gson
import org.openedx.auth.data.api.AuthApi
import org.openedx.auth.data.model.AuthResponse
import org.openedx.core.ApiConstants
import org.openedx.app.system.notifier.AppNotifier
import org.openedx.app.system.notifier.LogoutEvent
import kotlinx.coroutines.runBlocking
import okhttp3.*
import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONException
import org.json.JSONObject
import org.openedx.app.system.notifier.AppNotifier
import org.openedx.app.system.notifier.LogoutEvent
import org.openedx.auth.data.api.AuthApi
import org.openedx.auth.data.model.AuthResponse
import org.openedx.core.ApiConstants
import org.openedx.core.ApiConstants.TOKEN_TYPE_JWT
import org.openedx.core.BuildConfig
import org.openedx.core.BuildConfig.ACCESS_TOKEN_TYPE
import org.openedx.core.data.storage.CorePreferences
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
Expand Down Expand Up @@ -53,53 +55,54 @@ class OauthRefreshTokenAuthenticator(
val errorCode = getErrorCode(response.peekBody(200).string())
if (errorCode != null) {
when (errorCode) {
TOKEN_EXPIRED_ERROR_MESSAGE -> {
TOKEN_EXPIRED_ERROR_MESSAGE, JWT_TOKEN_EXPIRED -> {
try {
val newAuth = refreshAccessToken(refreshToken)
if (newAuth != null) {
return response.request.newBuilder()
.header(
"Authorization",
ApiConstants.TOKEN_TYPE_BEARER + " " + newAuth.accessToken
HEADER_AUTHORIZATION,
ACCESS_TOKEN_TYPE + " " + newAuth.accessToken
)
.build()
} else {
val actualToken = preferencesManager.accessToken
if (actualToken != accessToken) {
return response.request.newBuilder()
.header(
"Authorization",
ApiConstants.TOKEN_TYPE_BEARER + " " + actualToken
HEADER_AUTHORIZATION,
"$ACCESS_TOKEN_TYPE $actualToken"
)
.build()
}
return null
}

} catch (e: Exception) {
return null
}
}
TOKEN_NONEXISTENT_ERROR_MESSAGE, TOKEN_INVALID_GRANT_ERROR_MESSAGE -> {

TOKEN_NONEXISTENT_ERROR_MESSAGE, TOKEN_INVALID_GRANT_ERROR_MESSAGE, JWT_INVALID_TOKEN -> {
// Retry request with the current access_token if the original access_token used in
// request does not match the current access_token. This case can occur when
// asynchronous calls are made and are attempting to refresh the access_token where
// one call succeeds but the other fails. https://github.com/edx/edx-app-android/pull/834
val authHeaders =
response.request.headers["Authorization"]?.split(" ".toRegex())
val authHeaders = response.request.headers[HEADER_AUTHORIZATION]
?.split(" ".toRegex())
if (authHeaders?.toTypedArray()?.getOrNull(1) != accessToken) {
return response.request.newBuilder()
.header(
"Authorization",
ApiConstants.TOKEN_TYPE_BEARER + " " + accessToken
HEADER_AUTHORIZATION,
"$ACCESS_TOKEN_TYPE $accessToken"
).build()
}

runBlocking {
appNotifier.send(LogoutEvent())
}
}
DISABLED_USER_ERROR_MESSAGE -> {

DISABLED_USER_ERROR_MESSAGE, JWT_DISABLED_USER_ERROR_MESSAGE -> {
runBlocking {
appNotifier.send(LogoutEvent())
}
Expand All @@ -114,7 +117,8 @@ class OauthRefreshTokenAuthenticator(
val response = authApi.refreshAccessToken(
ApiConstants.TOKEN_TYPE_REFRESH,
BuildConfig.CLIENT_ID,
refreshToken
refreshToken,
ACCESS_TOKEN_TYPE
).execute()
val authResponse = response.body()
if (response.isSuccessful && authResponse != null) {
Expand All @@ -136,17 +140,21 @@ class OauthRefreshTokenAuthenticator(
private fun getErrorCode(responseBody: String): String? {
try {
val jsonObj = JSONObject(responseBody)
var errorCode = jsonObj.optString("error_code", "")
return if (errorCode != "") {
errorCode
if (jsonObj.has(FIELD_ERROR_CODE)) {
return jsonObj.getString(FIELD_ERROR_CODE)
} else {
errorCode = jsonObj
.optJSONObject("developer_message")
?.optString("error_code", "") ?: ""
if (errorCode != "") {
errorCode
return if (TOKEN_TYPE_JWT.equals(ACCESS_TOKEN_TYPE, ignoreCase = true)) {
val errorType = if (jsonObj.has(FIELD_DETAIL)) FIELD_DETAIL else FIELD_DEVELOPER_MESSAGE
jsonObj.getString(errorType)
} else {
null
val errorCode = jsonObj
.optJSONObject(FIELD_DEVELOPER_MESSAGE)
?.optString(FIELD_ERROR_CODE, "") ?: ""
if (errorCode != "") {
errorCode
} else {
null
}
}
}
} catch (ex: JSONException) {
Expand All @@ -156,9 +164,18 @@ class OauthRefreshTokenAuthenticator(
}

companion object {
private const val HEADER_AUTHORIZATION = "Authorization"

private const val TOKEN_EXPIRED_ERROR_MESSAGE = "token_expired"
private const val TOKEN_NONEXISTENT_ERROR_MESSAGE = "token_nonexistent"
private const val TOKEN_INVALID_GRANT_ERROR_MESSAGE = "invalid_grant"
private const val DISABLED_USER_ERROR_MESSAGE = "user_is_disabled"
private const val JWT_TOKEN_EXPIRED = "Token has expired."
private const val JWT_INVALID_TOKEN = "Invalid token."
private const val JWT_DISABLED_USER_ERROR_MESSAGE = "User account is disabled."

private const val FIELD_ERROR_CODE = "error_code"
private const val FIELD_DETAIL = "detail"
private const val FIELD_DEVELOPER_MESSAGE = "developer_message"
}
}
16 changes: 8 additions & 8 deletions auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ interface AuthApi {
@FormUrlEncoded
@POST(ApiConstants.URL_ACCESS_TOKEN)
suspend fun getAccessToken(
@Field("grant_type")
grantType: String,
@Field("client_id")
clientId: String,
@Field("username")
username: String,
@Field("password")
password: String,
@Field("grant_type") grantType: String,
@Field("client_id") clientId: String,
@Field("username") username: String,
@Field("password") password: String,
@Field("token_type") tokenType: String,
@Field("asymmetric_jwt") isAsymmetricJwt: Boolean = true,
): AuthResponse

@FormUrlEncoded
Expand All @@ -30,6 +28,8 @@ interface AuthApi {
@Field("grant_type") grantType: String,
@Field("client_id") clientId: String,
@Field("refresh_token") refreshToken: String,
@Field("token_type") tokenType: String,
@Field("asymmetric_jwt") isAsymmetricJwt: Boolean = true,
): Call<AuthResponse>

@GET(ApiConstants.URL_REGISTRATION_FIELDS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ class AuthRepository(
ApiConstants.GRANT_TYPE_PASSWORD,
org.openedx.core.BuildConfig.CLIENT_ID,
username,
password
password,
org.openedx.core.BuildConfig.ACCESS_TOKEN_TYPE
)
if (authResponse.error != null) {
throw EdxError.UnknownException(authResponse.error!!)
Expand Down
6 changes: 5 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#Set of environments
environments:
DEV:
URLS:
Expand Down Expand Up @@ -38,5 +39,8 @@ environments:
APPLICATION_ID: ""
API_KEY: ""
GCM_SENDER_ID: ""
#Platform names
platformName: "OpenEdX"
platformFullName: "OpenEdX"
platformFullName: "OpenEdX"
#tokenType enum accepts JWT and BEARER only
tokenType: "JWT"
2 changes: 2 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ android {

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"

buildConfigField "String", "ACCESS_TOKEN_TYPE", "\"${config.tokenType}\""
}

namespace 'org.openedx.core'
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/org/openedx/core/ApiConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ object ApiConstants {

const val GRANT_TYPE_PASSWORD = "password"

const val TOKEN_TYPE_REFRESH = "refresh_token"

const val TOKEN_TYPE_BEARER = "Bearer"
const val TOKEN_TYPE_JWT = "jwt"
const val TOKEN_TYPE_REFRESH = "refresh_token"

const val EMAIL = "email"
const val PASSWORD = "password"
Expand Down

0 comments on commit 99085e0

Please sign in to comment.