0

Im trying to authenticate to Cloudinary API service using the below code but i get 401 unauthorized error, it expects credentials in this format https://API_KEY:API_SECRET@..., when i substitute with actual values it works great with browser/postman but fails with retrofit2, below is my code.

// create and initialize retrofit2 client

public static OkHttpClient getClient(){

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(Level.BASIC);

OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.addHeader("API_KEY","API_SECRET")
.addHeader("Accept","Application/JSON").build();

return chain.proceed(request);

}
})
.addInterceptor(interceptor)
.build();

return client;
}

private static Retrofit retrofit = null;

public static Retrofit getClient(String baseUrl){
if (retrofit == null){

retrofit = new Retrofit.Builder()
.client(getClient())
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}

// Interface with get methods to access image resources
public interface CloudinaryService {

@GET("resources/image")
Call<imageresponse> getImageResource();
}

// Util class to make requests

public class ApiUtils {
private static final String BASE_URL = "http://api.cloudinary.com/v...";
public static CloudinaryService getImageService(){
return RetrofitClient.getClient(BASE_URL)
.create(CloudinaryService.class);
}

}

Any help fixing the error will be highly appreciated, not sure if need custom converter. thanks

***** Edit******

public static String credentials = Credentials.basic(API_KEY,API_SECRET);

      OkHttpClient client = new OkHttpClient.Builder()

//                .authenticator(new Authenticator() {
//                    @Override
//                    public Request authenticate(Route route, Response response) throws IOException {
//
//                        return response.request().newBuilder().header("Authorization", credentials).build();
//                    }
//                })
                .addInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {

                        Request request = (chain.request().newBuilder()
                                .header("Accept","Application/JSON")
                                .header("Cache-Control", "public, max-age=" + 60)
                                .header("Authorization",credentials).build());


                        return chain.proceed(request);

                    }
                })
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .addInterceptor(loggingInterceptor)
                .addInterceptor(provideOfflineCacheInterceptor())
                .addNetworkInterceptor(provideCacheInterceptor())
                .cache(getCache())
                .build();

        return client;
    }
  • You are not using Authentication here, which explains the error... How are you expecting to pass the credentials? Have you tried search for "Retrofit basic authentication"? – OneCricketeer Apr 26 '17 at 01:34
  • 1
    You don't pass that data as a header. https://futurestud.io/tutorials/android-basic-authentication-with-retrofit – OneCricketeer Apr 26 '17 at 01:40
  • **it expects credentials in this format https://API_KEY:API_SECRET@...,**. Could you explain more with this because I saw your `BASE_URL` different. – RoShan Shan Apr 26 '17 at 02:41

3 Answers3

0

I was able to fix the issue with adding authenticator to the builder.

.authenticator(new Authenticator() {
                    @Override
                    public Request authenticate(Route route, Response response) throws IOException {

                        return response.request().newBuilder().header("Authorization", credentials).build();
                    }
                })

thanks for all your help.

0
        request = chain.request();
        builder = request.newBuilder();

        if (TextUtils.isEmpty(request.header(AUTH)) && UserPreference.getInstance().isSignin())
            builder.addHeader(AUTH, UserPreference.getInstance().getAccessToken());

        if (NetUtil.hasNetwork(GridInnApplication.getInstance()))
            builder.header(USER_AGENT, userAgent);
        else
            builder.cacheControl(CacheControl.FORCE_CACHE);

        request = builder.build();
        Response response = chain.proceed(request);

        if (NetUtil.hasNetwork(GridInnApplication.getInstance())) {
            String cacheControl = request.cacheControl().toString();
            return response.newBuilder()
                    .header(CACHE_CONTROL, cacheControl)
                    .removeHeader(PRAGMA)
                    .build();
        } else {
            return response.newBuilder()
                    .addHeader(CACHE_CONTROL, CACHE_CONTROL_ONLY_CACHED)
                    .removeHeader(PRAGMA)
                    .build();
        }

//you can results before returing intercept

chen
  • 1
  • 2
0

The answer provided by leafNext will work but will cause every request to be sent twice - The authenticator only kicks in if the server responds with 401. You send the request, get 401 and then send it again with proper credentials.

The correct solution is to provide the credentials from the get go, using the interceptor. It's similar to what you tried to do originally, but you got the syntax wrong. The expected format is basic authentication.

.addInterceptor(new Interceptor() {
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        // Request customization: add request headers
        return chain.proceed(chain.request().newBuilder()
            .header("Authorization", credentials).build());
    }
});

Where credentials should follow the basic authentication protocol: Assuming the Api key is key and the secret is secret, you base64-encode the expression key:secret and prefix it with Basic. In this example the value of credentials should end up like so:

Basic a2V5OnNlY3JldA==


Edit - Added a fully working independent code bit to verify basic auth is working for okhttp (and thus with retrofit when using okhttp):

public int testBasicAuth() throws IOException {
    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request request = (chain.request().newBuilder()
                            .header("Authorization",okhttp3.Credentials.basic(KEY, SECRET)).build());
                    return chain.proceed(request);
                }
            }).build();

    Request request = new Request.Builder()
            .url("https://api.cloudinary.com/v1_1/[cloud_name]/resources/image")
            .build();

    int code = client.newCall(request).execute().code(); 
    return code; // 200
}
nitzanj
  • 1,699
  • 1
  • 10
  • 14
  • Thanks for the comment, i tried your code above but still it still returns 401, but works with authenticator, any clue ? – crazystarlord May 12 '17 at 01:38
  • I read other blogs and I also logged OKhttp calls i'm not sure if your theory of twice authentication is correct, it seems to intercept the call at the first attempt and authenticate just fine. – crazystarlord May 12 '17 at 01:55
  • See [this discussion](http://stackoverflow.com/questions/22490057/android-okhttp-with-basic-authentication). Authenticator is made specifically for retries as mentioned in the [cookbook](https://github.com/square/okhttp/wiki/Recipes). The [source](https://github.com/square/okhttp/blob/07309c1c7d9e296014268ebd155ebf7ef8679f6c/okhttp/src/main/java/okhttp3/internal/http/RetryAndFollowUpInterceptor.java) also shows `authenticate` is only called once a 401 response was already received. Anyway, did you verify your interceptor gets activated? can you post the updated code? – nitzanj May 12 '17 at 11:40
  • Yes, you're right but when i'm passing the authorization header via the interceptor i get 401, i added the recent code – crazystarlord May 13 '17 at 18:32
  • I edited my answer, added a code bit that definitely works (tested with my credentials). Test it out with your cloud name and credentials. If that doesn't work ping me, we'll continue it in chat. – nitzanj May 14 '17 at 18:11