I would like to create a simple app to check the abilities of newly launched Google Photos Library API (https://developers.google.com/photos/library/guides/get-started).
However I am facing an 401 error when trying to get album list. I've enabled photos library API in API Console, and in my app I've requested access to this scope using the following code:
val GOOGLE_PHOTOS_SCOPE = Scope("https://www.googleapis.com/auth/photoslibrary.readonly")
if (!GoogleSignIn.hasPermissions(
GoogleSignIn.getLastSignedInAccount(this),
GOOGLE_PHOTOS_SCOPE)) {
GoogleSignIn.requestPermissions(
this,
1,
GoogleSignIn.getLastSignedInAccount(this),
GOOGLE_PHOTOS_SCOPE)
} else {
Log.d(TAG, "Permission granted")
(application as QuizApp).photosLibraryApi.getAlbumList().enqueue(
object: retrofit2.Callback<ResponseBody> {
override fun onFailure(call: Call<ResponseBody>?, t: Throwable?) {
Log.e(TAG, "FAIL ${t.toString()}", t)
}
override fun onResponse(call: Call<ResponseBody>?, response: Response<ResponseBody>?) {
Log.d(TAG, "Success ${response.toString()}")
}
}
)
}
I guess I need to somehow provide my client_id and project_id from credentials.json file, but I have no idea how to do it. Anyone done it before? Any tips what's the best way to do Google's OAuth2.0?
After loggin in with the Google account (and this is async operation, so you need a listener - see Google login docs) you can get an authorisation code. This code you need to exchange for an access token, and this access token is then passed as a parameter to all your Google Photos API calls.
I just used a library like OKHTTP (on Android) to build the api request to get this accesstoken and yes, there you need your client key.
Note: JAVA code, but you can of course do the same in Kotlin with different syntax.
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = new FormEncodingBuilder()
.add("grant_type", "authorization_code")
.add("client_id", CLIENT_AUTH_KEY_WEB)
.add("client_secret", CLIENT_SECRET)
.add("redirect_uri", "")
.add("code", ACTUAL AUTHORISATION CODE AFTER LOGIN)
.build();
final Request request = new Request.Builder()
.url("https://www.googleapis.com/oauth2/v4/token")
.post(requestBody)
.build();
After this (and you need to have the listeners on for success or failure - see OKHTTP docs) then you can use the access token to get access to the API.
Of course it should also work with RetroFit as I see you are using, but I have no experience with that library.
Related
I'm using OkHttp with Kotlin in Android to query a REST API using basic auth.
When I use Postman to GET the resource I get:
200 when using basic auth -> So my credentials are correct and working
401 without using basic auth -> Expected behaviour (Unauthorized)
But when I use OkHttp I always get 403 with both preemptive and reactive authentication or even if I don't add any auth header at all.
Here's what I tried so far:
Adding "Authorization" header to request
Adding an Interceptor to client adding auth header to all requests
Adding an Authenticator to client
I only found this issue to be of any relevance, but I already use https instead of http in the URI. (Using http doesn't work either.)
My code to send the GET request:
private val client = OkHttpClient().newBuilder().build()
fun getOrders() {
val credentials = Credentials.basic(/*username*/, /*password*/)
val request = Request.Builder()
.url("https://example.com")
.addHeader("Authorization", credentials)
.build()
// Shows that request actually contains the header so that's OK
request.headers.forEach { println(it) }
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
println("Response STATUS: " + response.code)
}
})
}
I think I ruled out the problem being on the server's end with Postman handling all this correctly and I don't have any other idea what might work.
Situation
I am providing a Google login option in my android app using google's recommended GoogleSignInClient and GoogleSignInOptions.
The GoogleSignInOptions are specified like this:
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestServerAuthCode(webClientConfiguration.clientId.clientId)
.requestEmail()
.requestScopes(ScopesProvider.CLOUD_MANAGEMENT_SCOPE, ScopesProvider.FIREBASE_SCOPE)
.build()
These options mean:
we require an authorization code
the users email
a few scopes(not explained more detailed here)
and we are using the default sign in.
After the use logged in with his the callback gives us an GoogleSignInAccount object(see google documentation for more detailed infos).
What my app now needs is an access token as it needs access to certain resources that are included within the permission scopes shown above.
We simply request this token like it is specified in google's oauth2 documentation (using OkHttp3 in this example):
val httpUrl = HttpUrl.parse(webClientConfiguration.tokenUri.toString())!!.newBuilder()
.addQueryParameter("client_id", webClientConfiguration.clientId.clientId)
.addQueryParameter("client_secret", webClientConfiguration.clientId.clientSecret)
.addQueryParameter("code", account!!.serverAuthCode)
.addQueryParameter("grant_type", "authorization_code")
.addQueryParameter("redirect_uri", "http://localhost:1234")
.build()
val tokenRequest = Request.Builder()
.method(
"POST", RequestBody.create(
MediaType.parse("application/x-www-form-urlencoded"),
""
)
)
.url(httpUrl)
.build()
val client = OkHttpClient.Builder().build()
client.newCall(tokenRequest).enqueue(
object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: IOException) {
Log.d(LOGTAG, "token exchange request failed", e)
}
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
Log.d(LOGTAG, "repsonse! ${response.body()!!.string()}")
}
}
)
As specified in the docs, this will return a json object with all the required information:
{
"access_token": "<your-individual-access-token>",
"expires_in": 3599,
"id_token": "<user-signin-id-token>"
}
The problem
This request does not contain the required refreshToken. As the initial request(which contains the refreshtoken) which the GoogleSignIn takes care of is supposed to signal the server that we need an refresh token. As discussed here, we would need to add an access_type: offline parameter to the authentication request. But we can't edit the request, since the GoogleSignIn client takes care of this. How can we recieve an refresh token?
After researching for a long time I found a simple solution which works in my case. When creating the GoogleSignInOptions, we tell the GoogleSignIn that we need an authorization code:
.requestServerAuthCode(webClientConfiguration.clientId.clientId)
it turns out that you literally only have to add true as the second parameter("forceCodeForRefreshToken"):
.requestServerAuthCode(webClientConfiguration.clientId.clientId, true)
Then the client will return a different authorization code, which in return will make the google oauth server give you a refresh token when using the code in an request to get an access token.
I am working on an user app for a local charitable organization, and need to access their API. The API is from wild apricot, and this is the documentation for making a token request:
Authentication tokens are obtained from Wild Apricot's authentication service, which is located at https://oauth.wildapricot.org. This service adheres to oAuth 2.0.
This is the access option I need to implement:
-----------------In order to obtain access token with API key, you have to make the following request:
POST /auth/token HTTP/1.1
Host: oauth.wildapricot.org
Authorization: Basic BASE64_ENCODED("APIKEY:YOUR_API_KEY")
Content-type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=auto
-------------------------------So. finally your request will look like:
POST /auth/token HTTP/1.1
Host: oauth.wildapricot.org
Authorization: Basic QVBJS0VZOm85c2U4N3Jnb2l5c29lcjk4MDcwOS0=
Content-type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=auto
I am attempting to make this call with retrofit2, and an okhttp3 interceptor, and getting a bad request response (I am very much new and learning, and have not been able to get anything other response than a 400 bad request (when I use "/auth/token" as the endpoint), or a 404 not found (when I use "/auth/token HTTP/1.1" as the endpoint). If someone could tell me where exactly I am messing this up It would be greatly appreciated, the code I have tried is below.
Interface:
interface WAApiCall {
#POST("auth/token")
fun callPost(#Body body:String ): Call<AuthToken>
}
Call Service:
object WAApiCallService {
private const val API_KEY = "xxxxxxxxIxHavexAxValidxKeyxxxx"
private const val BASE_URL = "https://oauth.wildapricot.org/"
private val AUTH = "Basic" + Base64.encodeToString(API_KEY.toByteArray(), Base64.NO_WRAP)
private const val CONTENT_TYPE = "application/x-www-form-urlencoded"
private var api:WAApiCall? = null
private fun getWAApi(context: Context) : WAApiCall {
if(api==null){
val OkHttpClient = OkHttpClient.Builder()
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BASIC
OkHttpClient.addInterceptor{chain ->
val request = chain.request()
Log.d("CALL", request.body.toString())
val newRequest = request.newBuilder()
.addHeader("Host", "oauth.wildapricot.org")
.addHeader("Authorization", AUTH )
.addHeader("Content-type", CONTENT_TYPE)
.method(request.method, request.body)
.build()
chain.proceed(newRequest)
}
api = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.build())
.build()
.create(WAApiCall::class.java)
}
return api!!
}
fun call(context: Context) =
getWAApi(context)
}
Function in Main Activity to make the call:
fun testRequest(){
val call = WAApiCallService.call(this)
call.callPost("grant_type=client_credentials&scope=auto")
.enqueue(object: Callback<AuthToken>{
override fun onFailure(call: Call<AuthToken>, t: Throwable) {
Log.i("FAILURE", t.localizedMessage)
}
override fun onResponse(call: Call<AuthToken>, response: Response<AuthToken>) {
Log.i("SUCCESS", "TOKEN = ${response.body().toString()}")
Log.i("SUCCESS", "${response}")
val token = response.body()?.accessToken
Log.i("SUCCESS", "TOKEN = $token")
}
})
}
Error message:
I/SUCCESS: TOKEN = null
I/SUCCESS: Response{protocol=http/1.1, code=400, message=Bad Request, url=https://oauth.wildapricot.org/auth/token}
I think that I am just not understanding how to implement this type of request in some basic way, I could not get it to work in Postman either. I understand that I need to send the credentials to the authentication server, and receive an access token, that will expire and need to be refreshed, and that It will be included in each actual API endpoint call, I guess I'm just missing something crucial in the most important step of that process (getting the actual token, I am imagining it is a simple, forehead slapping kind of misunderstanding on my part?). The wild apricot API is on swagger hub, and I am able to gain access through that UI, with my API key, and see the responses, so I know that it is valid.
Your client credentials request looks mostly all good. The only thing I can see that looks wrong is no space character in the AUTH header between 'Basic' and the encoded credential.
If that doesn't work, could you trace the HTTP request and verify that you are sending the message you think you are.
Thank you for that observation, it led me to figuring out what ultimately was wrong in my initial attempt. After adding that space, I traced the request and found that It was actually sending two headers for content type.
The fix for that was to set the header in the retrofit call from the interface:
interface WAApiCall {
#POST("auth/token")
fun callPost(#Body Body: okhttp3.RequestBody, #Header("Content-type") type: String): Call<AuthToken>
}
As you can see the body is also slightly different, the call was getting through but was returning:
"unsupported_grant_type".
I was passing a raw string as the body parameter, which was including the quotation marks in the request. The solution there was to pass the okhttp3.Request body type rather than a raw string, in the function that makes the actual call it looks like this:
val body: "grant_type=client_credentials&scope=auto&obtain_refresh_token=true"
val requestBody = RequestBody.create("text/plain".toMediaTypeOrNull(),body)
val call = WAApiCallService.call(this)
call.callPost(requestBody,"application/x-www-form-urlencoded")
.enqueue(object: Callback<AuthToken>{
With those changes the call succeeds and my long running headache is over.
When i try to get token with client id, client secret key and authorization code with help of retrofit. I'm getting 401 UnAuthorized error
MainActivity.kt
if (appLinkData != null && appLinkData.toString().startsWith("https://abc.abconline.com/plugin/callback")) {
val code = appLinkData.getQueryParameter("code")
// Toast.makeText(this, code, Toast.LENGTH_SHORT).show()
val builder = Retrofit.Builder().baseUrl("https://abc.abcOnline.com")
.addConverterFactory(GsonConverterFactory.create()).build()
val client = builder.create(restClient::class.java)
client.getAccessToken(
clientId,
clientSecret,
code,
"Authorization"
).enqueue(object : Callback<AccessToken> {
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
Toast.makeText(applicationContext, "Falied: $t.message", Toast.LENGTH_SHORT)
.show()
}
override fun onResponse(call: Call<AccessToken>, response: Response<AccessToken>) {
Toast.makeText(
applicationContext,
response.body().toString(),
Toast.LENGTH_SHORT
).show()
}
RestClient.kt
interface restClient {
#POST("/oauth2/token")
#FormUrlEncoded
fun getAccessToken(
#Field("client_id") clientId: String,
#Field("client_secret") clientSecret: String,
#Field("code") code: String,
#Field("grant_type") grantType: String,
#Field("redirect_uri") redirectUri: String
): Call<AccessToken>
}
How I can fix it?
I am spending a lot of time to figure out...
Thank you
It's not the problem with Retrofit, Server is returning its response 401
means call is successful but there might be a problem with "Credentials" or insufficient " Privileges", Use POSTMAN for testing if the response is Succesful then implement it.
have a look at 401-unauthorized-error
hope it helps.
As a general point, integrating Open Id Connect into a mobile app is not easy. There are many tricky aspects related to usability and reliability. I think you should be looking for these qualities:
The ability to view API and OAuth messages
Use a mainstream and respected security library
If it helps I have some resources here that may help you to compare against, and a code sample you can run. Maybe you can adapt Retrofit around this type of solution?
Kotlin AppAuth Sample
Android Code Sample Write Up
Android HTTPS Debugging Write Up
I am trying to post data to REST API server with retrofit + RxJava . When I am trying to send data to server , it said " HTTP 500 Internal Server Error Occurred". But when the data is send with POSTMAN, it succeeded.
This is the function for sending data in Model.
// Encountering with 500 server error
fun postSchedule(data : ScheduleResponse , errorLD: MutableLiveData<String>){
Log.d("POST DATA", "${data.title} ${data.remindMeAt}" )
userClient.postScheduleItem(data)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.unsubscribeOn(Schedulers.io())
.subscribe(object : io.reactivex.Observer<ServerResponse>{
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: ServerResponse) {
errorLD.value = t.status
}
override fun onError(e: Throwable) {
errorLD.value = e.message
}
})
}
This is my API interface
#Headers("Accept: application/json")
#POST("schedules")
fun postScheduleItem(#Body data: ScheduleResponse): Observable<ServerResponse>
This is the retrofit client.
val httpLoggingInterceptor = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
val httpClient = OkHttpClient.Builder()
var dbInstance: TodoDB = TodoDB.getInstance(context)
var rxJavaAdapter = RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())
val retrofitBuilder =
Retrofit.Builder()
.baseUrl(AppConstants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(rxJavaAdapter)
fun <T> createService(serviceClass: Class<T>, authToken: String?): T {
if (!TextUtils.isEmpty(authToken)) {
val interceptor = AuthenticationInterceptor(authToken!!)
if (!httpClient.interceptors().contains(interceptor)) {
httpClient.addInterceptor(interceptor)
retrofitBuilder.client(httpClient.build())
}
}
return retrofitBuilder.build().create(serviceClass)
}
Please help me with this.Thank you.
Client side code is not enough to determine what causes the server to respond with 500. The best you can do is start debugging the issue.
There are several directions you can go:
If you have access to the server or know someone who does, you could debug the server and determine what causes the Internal server error. Maybe the server logs can help as well and you don't have to actually step through the server code.
If you don't have access to the server, you could look at the body of the server response. Maybe there's a detailed error description there in html, json or some other format that will help you find out the root cause.
If the above steps don't help then it's very useful that you know the request works with POSTMAN. You can compare the exact POSTMAN request with the exact Retrofit request, header by header,
line by line. To do that, you should first add your httpLoggingInterceptor to your okhttp client builder with
val httpClient = OkHttpClient.Builder().addNetworkInterceptor(httpLoggingInterceptor)
and look for the request log in logcat.
If you spot the differences between the working and the not working requests, then you should work your way through all the differences, and adjust the retrofit request by adding or modifying headers using okhttp interceptors so that, at the end, the retrofit request looks exactly the same as the POSTMAN request. I suggest you remove the AuthenticationInterceptor at first and simulate it "manually" with a custom interceptor and a hard coded auth token.
Retry the request every time you eliminate a difference to isolate the cause of the internal server error.
Hope this helps!