package org.edx.mobile.http; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.inject.Inject; import org.edx.mobile.authentication.LoginService; import org.edx.mobile.authentication.AuthResponse; import org.edx.mobile.logger.Logger; import org.edx.mobile.module.prefs.LoginPrefs; import org.edx.mobile.util.Config; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import okhttp3.Authenticator; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; import roboguice.RoboGuice; /** * Authenticator for 401 responses for refreshing oauth tokens. Checks for * the expired oauth token case and then uses the refresh token to retrieve a * new access token. Using the new access token, the original http request * that received the 401 will be attempted again. If no refresh_token is * present, no authentication attempt is made. */ public class OauthRefreshTokenAuthenticator implements Authenticator { private final Logger logger = new Logger(getClass().getName()); private final static String TOKEN_EXPIRED_ERROR_MESSAGE = "token_expired"; private final static String TOKEN_NONEXISTENT_ERROR_MESSAGE = "token_nonexistent"; private final static String TOKEN_INVALID_GRANT_ERROR_MESSAGE = "invalid_grant"; private Context context; @Inject Config config; @Inject LoginPrefs loginPrefs; public OauthRefreshTokenAuthenticator(Context context) { this.context = context; RoboGuice.injectMembers(context, this); } @Override public synchronized Request authenticate(Route route, final Response response) throws IOException { logger.warn(response.toString()); final AuthResponse currentAuth = loginPrefs.getCurrentAuth(); if (null == currentAuth || null == currentAuth.refresh_token) { return null; } String errorCode = getErrorCode(response.peekBody(200).string()); if (errorCode != null) { switch (errorCode) { case TOKEN_EXPIRED_ERROR_MESSAGE: final AuthResponse refreshedAuth; try { refreshedAuth = refreshAccessToken(currentAuth); } catch (HttpResponseStatusException e) { return null; } return response.request().newBuilder() .header("Authorization", refreshedAuth.token_type + " " + refreshedAuth.access_token) .build(); case TOKEN_NONEXISTENT_ERROR_MESSAGE: case TOKEN_INVALID_GRANT_ERROR_MESSAGE: // 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 if (!response.request().headers().get("Authorization").split(" ")[1].equals(currentAuth.access_token)) { return response.request().newBuilder() .header("Authorization", currentAuth.token_type + " " + currentAuth.access_token) .build(); } } } return null; } @NonNull private AuthResponse refreshAccessToken(AuthResponse currentAuth) throws IOException, HttpResponseStatusException { OkHttpClient client = OkHttpUtil.getClient(context); Retrofit retrofit = new Retrofit.Builder() .client(client) .baseUrl(config.getApiHostURL()) .addConverterFactory(GsonConverterFactory.create()) .build(); LoginService loginService = retrofit.create(LoginService.class); retrofit2.Response<AuthResponse> refreshTokenResponse; refreshTokenResponse = loginService.refreshAccessToken("refresh_token", config.getOAuthClientId(), currentAuth.refresh_token).execute(); if (!refreshTokenResponse.isSuccessful()) { throw new HttpResponseStatusException(refreshTokenResponse.code()); } AuthResponse refreshTokenData = refreshTokenResponse.body(); loginPrefs.storeRefreshTokenResponse(refreshTokenData); return refreshTokenData; } @Nullable private String getErrorCode(String responseBody) { try { JSONObject jsonObj = new JSONObject(responseBody); return jsonObj.getString("error_code"); } catch (JSONException ex) { logger.warn("Unable to get error_code from 401 response"); return null; } } }