How to call API Gateway with Cognito Credentials through retrofit2 on Android? - android

I use retrofit2 in my android apps for any http/rest call. Now I need to call an api generated with Amazon AWS API Gateway.
The AWS documentation say I should generate the client code throw the API Gateway console and use the class ApiClientFactory to build the request:
ApiClientFactory factory = new ApiClientFactory();
// Use CognitoCachingCredentialsProvider to provide AWS credentials
// for the ApiClientFactory
AWSCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(
context, // activity context
"identityPoolId", // Cognito identity pool id
Regions.US_EAST_1 // region of Cognito identity pool
};
factory.credentialsProvider(credentialsProvider);
// Create an instance of your SDK (this should come from the generated code).
final MyApiClient client = factory.build(MyApiClient.class);
// Invoke a method (e.g., 'parentPath1Get(param1,body)') exposed by your SDK.
// Here the method's return type is OriginalModel.
OriginalModel output = client.parentPath1Get(param1,body);
// You also have access to your API's models.
OriginalModel myModel = new OriginalModel();
myModel.setStreetAddress(streetAddress);
myModel.setCity(city);
myModel.setState(state);
myModel.setStreetNumber(streetNumber);
myModel.setNested(nested);
myModel.setPoBox(poBox);
Instead I would like to define the API like I would with retrofit: with an interface I write, connect it to RxJava, OkHttp etc...
My question is: how can I sign the retrofit requests with Cognito Identity Provider?

It took me several days to figure out how to make it work. Don't know why they don't point out the class instead of dozen of document pages. There are 4 steps in total, you must call in worker thread, I am using Rxjava but you can use AsyncTask instead:
Observable.create((Observable.OnSubscribe<String>) subscriber -> {
//Step 1: Get credential, ask server team for Identity pool id and regions
CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(
this, // Context
"Identity Pool ID", // Identity Pool ID
Regions.US_EAST_1 // Region
);
//Step 2: Get these 3 three keys, test with postman v4.9.3 to see if identity is correct
String identityId = credentialsProvider.getIdentityId();
Log.show("identityId = " + identityId);
String AccessKey = credentialsProvider.getCredentials().getAWSAccessKeyId();
String SecretKey = credentialsProvider.getCredentials().getAWSSecretKey();
String SessionKey = credentialsProvider.getCredentials().getSessionToken();
Log.show("AccessKey = " + AccessKey);
Log.show("SecretKey = " + SecretKey);
Log.show("SessionKey = " + SessionKey);
//Step 3: Create an aws requets and sign by using AWS4Signer class
AmazonWebServiceRequest amazonWebServiceRequest = new AmazonWebServiceRequest() {
};
ClientConfiguration clientConfiguration = new ClientConfiguration();
String API_GATEWAY_SERVICE_NAME = "execute-api";
Request request = new DefaultRequest(amazonWebServiceRequest,API_GATEWAY_SERVICE_NAME);
request.setEndpoint(URI.create("YOUR_URI"));
request.setHttpMethod(HttpMethodName.GET);
AWS4Signer signer = new AWS4Signer();
signer.setServiceName(API_GATEWAY_SERVICE_NAME);
signer.setRegionName(Region.getRegion(Regions.US_EAST_1).getName());
signer.sign(request, credentialsProvider.getCredentials());
Log.show("Request header " + request.getHeaders().toString());
//Step 4: Create new request with authorization headers
OkHttpClient httpClient = new OkHttpClient();
Map<String, String> headers = request.getHeaders();
List<String> key = new ArrayList<String>();
List<String> value = new ArrayList<String>();
for (Map.Entry<String, String> entry : headers.entrySet())
{
key.add(entry.getKey());
value.add(entry.getValue());
}
try {
okhttp3.Request request2 = new okhttp3.Request.Builder()
.url("Your_url") // remember to add / to the end of the url, otherwise the signature will be different
.addHeader(key.get(0), value.get(0))
.addHeader(key.get(1), value.get(1))
.addHeader(key.get(2), value.get(2))
.addHeader(key.get(3), value.get(3))
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.build();
Response response = null;
response = httpClient.newCall(request2).execute();
String body = response.body().string();
Log.show("response " + body);
} catch (Exception e) {
Log.show("error " + e);
}
subscriber.onNext(identityId);
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Subscriber<String>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
Log.show("Throwable = " + e.getMessage());
}
#Override
public void onNext(String s) {
}
});
The key here is the AWS4Signer class does 4 steps as documented here, you don't need to build one from scratch. In order to use AWS4Signer and AmazonWebServiceRequest, you need to import aws sdk in gradle:
compile 'com.amazonaws:aws-android-sdk-cognito:2.3.9'

Created an OkHttp interceptor based on #thanhbinh84 answer. Give it a try: https://github.com/Ghedeon/AwsInterceptor

The signature process is documented here: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
But you could probably try to reuse some of the code from the core runtime package that the default API Gateway client depends on. There may be libraries out there already for signing requests of type RxJava or OkHttp since the signature process is well-known.

Related

InvalidConfigurationException: Identity pool isn't set up for SNS using AmazonCognitoSync service

I have been trying to use AWS SDKs for Push Notifications. But I am getting errors. Tried to find a solution, but can't find much support for this.
iOS & Web push notifications are working fine
What all is already setup & done:
AWS back-end & console setting in place.
Identity Pool Id & other keys in place.
ARN topic in place.
Android side:
AWS SDK dependencies:
implementation 'com.amazonaws:aws-android-sdk-core:2.16.8'
implementation 'com.amazonaws:aws-android-sdk-cognito:2.6.23'
implementation 'com.amazonaws:aws-android-sdk-s3:2.15.1'
implementation 'com.amazonaws:aws-android-sdk-ddb:2.2.0'
implementation ('com.amazonaws:aws-android-sdk-mobile-client:2.16.8') { transitive = true; }
minSdkVersion 21
targetSdkVersion 29
Inside onCreate:
CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(
getApplicationContext(),
"My Pool Id here", // Identity pool ID
Regions.US_EAST_1 // Region
);
CognitoSyncManager client = new CognitoSyncManager(
LoginActivateActivity.this,
Regions.US_EAST_1,
credentialsProvider);
String registrationId = "MY_FCM_DEVICE_TOKEN"; **Instead of GCM ID, I am passing my unique FCM device token here. I searched, & it seems that wherever GCM is required, it is being replaced by FCM.**
try {
client.registerDevice("GCM", registrationId);
} catch (RegistrationFailedException rfe) {
Log.e("TAG", "Failed to register device for silent sync", rfe);
} catch (AmazonClientException ace) {
Log.e("TAG", "An unknown error caused registration for silent sync to fail", ace);
}
Dataset trackedDataset = client.openOrCreateDataset("My Topic here");
if (client.isDeviceRegistered()) {
try {
trackedDataset.subscribe();
} catch (SubscribeFailedException sfe) {
Log.e("TAG", "Failed to subscribe to datasets", sfe);
} catch (AmazonClientException ace) {
Log.e("TAG", "An unknown error caused the subscription to fail", ace);
}
}
Error I am getting on client.registerDevice("GCM", registrationId);
Caused by: com.amazonaws.services.cognitosync.model.InvalidConfigurationException: Identity pool isn't set up for SNS (Service: AmazonCognitoSync; Status Code: 400; Error Code: InvalidConfigurationException; Request ID: a858aaa2-**************************)
Note:
I tried using Amplify libraries, but even that didn't work. Also, at iOS & Web end they are using AWS SDK. So I am also bound to use the same. This is not even a device specific error.
All I need to do is setup my project to get push notifications. But I am stuck at the initial step. Not able to create an endpoint for Android device.
I actually found the solution to the issue, thanks to a friend who shared this link:
https://aws.amazon.com/premiumsupport/knowledge-center/create-android-push-messaging-sns/
This Youtube video also helped a lot:
https://www.youtube.com/watch?v=9QSO3ghSUNk&list=WL&index=3
Edited code
private void registerWithSNS() {
CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(
getApplicationContext(),
"Your Identity Pool ID",
Regions.US_EAST_1 // Region
);
client = new AmazonSNSClient(credentialsProvider);
String endpointArn = retrieveEndpointArn();
String token = "Your FCM Registration ID generated for the device";
boolean updateNeeded = false;
boolean createNeeded = (null == endpointArn || "".equalsIgnoreCase(endpointArn));
if (createNeeded) {
// No platform endpoint ARN is stored; need to call createEndpoint.
endpointArn = createEndpoint(token);
createNeeded = false;
}
System.out.println("Retrieving platform endpoint data...");
// Look up the platform endpoint and make sure the data in it is current, even if
// it was just created.
try {
GetEndpointAttributesRequest geaReq =
new GetEndpointAttributesRequest()
.withEndpointArn(endpointArn);
GetEndpointAttributesResult geaRes =
client.getEndpointAttributes(geaReq);
updateNeeded = !geaRes.getAttributes().get("Token").equals(token)
|| !geaRes.getAttributes().get("Enabled").equalsIgnoreCase("true");
} catch (NotFoundException nfe) {
// We had a stored ARN, but the platform endpoint associated with it
// disappeared. Recreate it.
createNeeded = true;
} catch (AmazonClientException e) {
createNeeded = true;
}
if (createNeeded) {
createEndpoint(token);
}
System.out.println("updateNeeded = " + updateNeeded);
if (updateNeeded) {
// The platform endpoint is out of sync with the current data;
// update the token and enable it.
System.out.println("Updating platform endpoint " + endpointArn);
Map attribs = new HashMap();
attribs.put("Token", token);
attribs.put("Enabled", "true");
SetEndpointAttributesRequest saeReq =
new SetEndpointAttributesRequest()
.withEndpointArn(endpointArn)
.withAttributes(attribs);
client.setEndpointAttributes(saeReq);
}
}
/**
* #return never null
* */
private String createEndpoint(String token) {
String endpointArn = null;
try {
System.out.println("Creating platform endpoint with token " + token);
CreatePlatformEndpointRequest cpeReq =
new CreatePlatformEndpointRequest()
.withPlatformApplicationArn("Your Platform ARN. This you get from AWS Console. Unique for all devices for a platform.")
.withToken(token);
CreatePlatformEndpointResult cpeRes = client
.createPlatformEndpoint(cpeReq);
endpointArn = cpeRes.getEndpointArn();
} catch (InvalidParameterException ipe) {
String message = ipe.getErrorMessage();
System.out.println("Exception message: " + message);
Pattern p = Pattern
.compile(".*Endpoint (arn:aws:sns[^ ]+) already exists " +
"with the same [Tt]oken.*");
Matcher m = p.matcher(message);
if (m.matches()) {
// The platform endpoint already exists for this token, but with
// additional custom data that
// createEndpoint doesn't want to overwrite. Just use the
// existing platform endpoint.
endpointArn = m.group(1);
} else {
// Rethrow the exception, the input is actually bad.
throw ipe;
}
}
storeEndpointArn(endpointArn);
return endpointArn;
}
/**
* #return the ARN the app was registered under previously, or null if no
* platform endpoint ARN is stored.
*/
private String retrieveEndpointArn() {
// Retrieve the platform endpoint ARN from permanent storage,
// or return null if null is stored.
return endpointArn;
}
/**
* Stores the platform endpoint ARN in permanent storage for lookup next time.
* */
private void storeEndpointArn(String endpointArn) {
// Write the platform endpoint ARN to permanent storage.
UserSession.getSession(LoginActivateActivity.this).setARN(endpointArn); //Your platform endpoint ARN. This is unique for each device, but changes when
}
Once an endpoint is created for the device, you need to store the endpointArn & FCM registration ID to your DB on server-side. Rest of the code will be your FCM implementation code for receiving notifications.
Hope this helps someone

How to set OIDC provider for AWSAssumeRoleWebIdentity

I am developing an android app which uses firebase authentication for signin and uses AWS S3 and dynamodb for managing data/images. I am trying to delegate an access to AWS resource via AWSAssumeRoleWebIdentity. The reason I am doing this is AWS Sign-In UI does not allow enough customization for UI and UI flow. I decided to use firebase authentication only for sign-in and sign-up.
Please find the source code and OIDC Provider setting. With them the error log is
No OpenIDConnect provider found in your account for https://securetoken.google.com/[project-name] (Service: AWSSecurityTokenService; Status Code: 400; Error Code: InvalidIdentityToken; Request ID: 37607060-9e1c-11e8-8ae0-636eae27c3bf)
Identity Provider of AWS IAM has been created with the name of "securetoken.google.com/[my-project-name]/" with the Thumbprint that I created referring to [1] and OAuth 2.0 client IDs obtained in Credentials of Google Cloud Service API & Services.
The source code is shown below.
public void uploadImageFile() {
CustomLog.logI("start of uploadImageFile");
setIDToken();
}
private void setIDToken() {
FirebaseUser mUser = FirebaseAuth.getInstance().getCurrentUser();
// To get ID Token of the user authenticated by google authentication
mUser.getIdToken(true)
.addOnCompleteListener(new OnCompleteListener<GetTokenResult>() {
public void onComplete (#NonNull Task< GetTokenResult > task) {
if (task.isSuccessful()) {
// Token information is set to mIDToken of the global variable
mIDToken = task.getResult().getToken();
AsyncTaskForAssumeRole asyncTaskForAssumeRole = new AsyncTaskForAssumeRole();
asyncTaskForAssumeRole.execute();
} else {
CustomLog.logE(task.getException().getMessage());
}
}
});
}
public class AsyncTaskForAssumeRole extends AsyncTask<Void, Void, BasicSessionCredentials> {
protected BasicSessionCredentials doInBackground(Void... params) {
try {
// set credentials from AssumeRoleWithWebIdentity
BasicSessionCredentials credentials = setAssumeRoleWithWebIdentity();
return credentials;
} catch (Exception e) {
CustomLog.logE(e.getMessage());
return null;
}
}
protected void onPostExecute(BasicSessionCredentials credentials) {
// upload file with S3 connection
connectToS3ForUpload(credentials);
}
}
private BasicSessionCredentials setAssumeRoleWithWebIdentity(){
CustomLog.logD("start of setAssumeRoleWithWebIdentity");
String ROLE_ARN = [my role arn];
// set AssumeRoleWithWebIdentity request with created policy and token information retrieved through Google Sign in information
AssumeRoleWithWebIdentityRequest request = new AssumeRoleWithWebIdentityRequest()
.withWebIdentityToken(mIDToken)
.withRoleArn(ROLE_ARN)
.withRoleSessionName("wifsession");
BasicAWSCredentials basicCreds = new BasicAWSCredentials("", "");
AWSSecurityTokenServiceClient sts = new AWSSecurityTokenServiceClient(basicCreds);
AssumeRoleWithWebIdentityResult result = sts.assumeRoleWithWebIdentity(request);
Credentials stsCredentials = result.getCredentials();
String subjectFromWIF = result.getSubjectFromWebIdentityToken();
BasicSessionCredentials credentials = new BasicSessionCredentials(stsCredentials.getAccessKeyId(),
stsCredentials.getSecretAccessKey(),
stsCredentials.getSessionToken());
return credentials;
}
Great thanks in advance.
[1] http://docs.aws.amazon.com/cli/latest/reference/iam/create-open-id-connect-provider.html
Consider using Amazon Cognito Federated Identities (Identity Pools) to federate (map) the user from your Identity Provider into Amazon Cognito and obtain a Cognito Identity Id, which can be used to authorize access to AWS resources. See https://docs.aws.amazon.com/cognito/latest/developerguide/open-id.html for further details.
Map<String, String> logins = new HashMap<String, String>();
logins.put("login.provider.com", token);
credentialsProvider.setLogins(logins);
Now, you can use the credentialsProvider object with an Amazon S3 client.
AmazonS3 s3Client = new AmazonS3Client(getApplicationContext(), credentialsProvider);

Send JWT token using AWS Android SDK

We have some API Gateway endpoints. Each endpoint is protected with a Custom Authorizer (CA). This CA inspect the HTTP call and verify the presence of the Authorization header. This header must contains an OpenId Connect token (a simple JWT token), so the CA can inspect it and make some checks and validations.
When we use Postman to invoke the endpoint, it works without a problem, since we can setup the right headers.
The problems start to raises when we use the generated Android SDK to make the same call, since each attempt to make the call sends an AWS4 signature as header. We can figure out how to send Authorization header with JWT.
We get what we need by extending ApiClientFactory class and adding the header in an explicit ways:
public class CustomApiClientFactory extends ApiClientFactory {
private String LOGIN_NAME = "a.provider.com";
private AWSCredentialsProvider provider;
#Override
public ApiClientFactory credentialsProvider(AWSCredentialsProvider provider) {
this.provider = provider;
return super.credentialsProvider(provider);
}
#Override
ApiClientHandler getHandler(String endpoint, String apiName) {
final Signer signer = provider == null ? null : getSigner(getRegion(endpoint));
// Ensure we always pass a configuration to the handler
final ClientConfiguration configuration = new ClientConfiguration();
return new ApiClientHandler(endpoint, apiName, signer, provider, null, configuration) {
#Override
Request<?> buildRequest(Method method, Object[] args) {
Request<?> request = super.buildRequest(method, args);
request.addHeader("Authorization", "Bearer " + ((CognitoCachingCredentialsProvider) provider).getLogins().get(LOGIN_NAME));
return request;
}
};
}
}
Even if it works, it sounds to me like a workaround. There is some known best practice in facing this problem?
API Gateway allows the request to be defined for each method just add a HTTP request header in the method request and the generated SDK will have a field to specify the header.
Console:
Generated Android SDK:
#com.amazonaws.mobileconnectors.apigateway.annotation.Operation(path = "/companies", method = "GET")
CompanyList companiesGet(
#com.amazonaws.mobileconnectors.apigateway.annotation.Parameter(name = "x-vendor-authorization", location = "header")
String xVendorAuthorization,
#com.amazonaws.mobileconnectors.apigateway.annotation.Parameter(name = "continuationToken", location = "query")
String continuationToken,
#com.amazonaws.mobileconnectors.apigateway.annotation.Parameter(name = "pageSize", location = "query")
String pageSize,
#com.amazonaws.mobileconnectors.apigateway.annotation.Parameter(name = "properties", location = "query")
String properties);
SDK call:
client.companiesGet("abc123AuthToken", "continueToken1", "20", "prop1");

Android: What is transport and jsonFactory in GoogleIdTokenVerifier.Builder?

in the blow code, whats is transport and jsonFactory ? (I do not understand)
https://developers.google.com/identity/sign-in/android/backend-auth#using-a-google-api-client-library
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
...
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport /**Here**/, jsonFactory /**Here**/)
.setAudience(Arrays.asList(CLIENT_ID))
// If you retrieved the token on Android using the Play Services 8.3 API or newer, set
// the issuer to "https://accounts.google.com". Otherwise, set the issuer to
// "accounts.google.com". If you need to verify tokens from multiple sources, build
// a GoogleIdTokenVerifier for each issuer and try them both.
.setIssuer("https://accounts.google.com")
.build();
// (Receive idTokenString by HTTPS POST)
GoogleIdToken idToken = verifier.verify(idTokenString);
if (idToken != null) {
Payload payload = idToken.getPayload();
// Print user identifier
String userId = payload.getSubject();
System.out.println("User ID: " + userId);
// Get profile information from payload
String email = payload.getEmail();
boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
String locale = (String) payload.get("locale");
String familyName = (String) payload.get("family_name");
String givenName = (String) payload.get("given_name");
// Use or store profile information
// ...
} else {
System.out.println("Invalid ID token.");
}
Since all the other answers are blah blah blah, here's a short answer:
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
GoogleIdTokenVerifier verifier =
new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory());
The GoogleIdTokenVerifier.Builder returns a GoogleIdTokenVerifier that will make a request to the tokeninfo endpoint with the transport you give it and use the JSONFactory to create a parser to parse the response.
Here is an example of an authenticator for a Cloud Endpoints project that uses the GoogleIdTokenVerifier.Builder
public class GoogleAuthenticator implements Authenticator {
private static final Logger log = Logger.getLogger(GoogleAuthenticator.class.getName());
private static final JacksonFactory jacksonFactory = new JacksonFactory();
// From: https://developers.google.com/identity/sign-in/android/backend-auth#using-a-google-api-client-library
// If you retrieved the token on Android using the Play Services 8.3 API or newer, set
// the issuer to "https://accounts.google.com". Otherwise, set the issuer to
// "accounts.google.com". If you need to verify tokens from multiple sources, build
// a GoogleIdTokenVerifier for each issuer and try them both.
GoogleIdTokenVerifier verifierForNewAndroidClients = new GoogleIdTokenVerifier.Builder(UrlFetchTransport.getDefaultInstance(), jacksonFactory)
.setAudience(Arrays.asList(CRLConstants.IOS_CLIENT_ID, CRLConstants.ANDROID_CLIENT_ID_RELEASE, CRLConstants.ANDROID_CLIENT_ID_DEBUG))
.setIssuer("https://accounts.google.com")
.build();
GoogleIdTokenVerifier verifierForOtherClients = new GoogleIdTokenVerifier.Builder(UrlFetchTransport.getDefaultInstance(), jacksonFactory)
.setAudience(Arrays.asList(CRLConstants.IOS_CLIENT_ID, CRLConstants.ANDROID_CLIENT_ID_RELEASE, CRLConstants.ANDROID_CLIENT_ID_DEBUG))
.setIssuer("accounts.google.com")
.build();
// Custom Authenticator class for authenticating google accounts
#Override
public User authenticate(HttpServletRequest request) {
String token = request.getHeader("google_id_token");
if (token != null) {
GoogleIdToken idToken = null;
try {
idToken = verifierForNewAndroidClients.verify(token);
if(idToken == null) idToken = verifierForOtherClients.verify(token);
if (idToken != null) {
GoogleIdToken.Payload payload = idToken.getPayload();
// Get profile information from payload
String userId = payload.getSubject();
String email = payload.getEmail();
return new GoogleUser(userId, email);
} else {
log.warning("Invalid Google ID token.");
}
} catch (GeneralSecurityException e) {
log.warning(e.getLocalizedMessage());
} catch (IOException e) {
log.warning(e.getLocalizedMessage());
}
}
return null;
}
}
You need to select transport according to the platform on which you are running the code.
Quoting from the documentation
Implementation is thread-safe, and sub-classes must be thread-safe. For maximum efficiency, applications should use a single globally-shared instance of the HTTP transport.
The recommended concrete implementation HTTP transport library to use depends on what environment you are running in:
Google App Engine: use com.google.api.client.extensions.appengine.http.UrlFetchTransport.
com.google.api.client.apache.ApacheHttpTransport doesn't work on App Engine because the Apache HTTP Client opens its own sockets (though in theory there are ways to hack it to work on App Engine that might work).
com.google.api.client.javanet.NetHttpTransport is discouraged due to a bug in the App Engine SDK itself in how it parses HTTP headers in the response.
Android:
For maximum backwards compatibility with older SDK's use newCompatibleTransport from com.google.api.client.extensions.android.http.AndroidHttp (read its JavaDoc for details).
If your application is targeting Gingerbread (SDK 2.3) or higher, simply use com.google.api.client.javanet.NetHttpTransport.
Other Java environments
com.google.api.client.javanet.NetHttpTransport is based on the HttpURLConnection built into the Java SDK, so it is normally the preferred choice.
com.google.api.client.apache.ApacheHttpTransport is a good choice for users of the Apache HTTP Client, especially if you need some of the configuration options available in that library.
Documentation Link: https://developers.google.com/api-client-library/java/google-http-java-client/reference/1.19.0/com/google/api/client/http/HttpTransport?is-external=true
If you blindly follow the 2nd answer to the question, you will get the exception Caused by: java.lang.ClassNotFoundException: com.google.appengine.api.urlfetch.HTTPMethod
JacksonFactory is deprecated. So this works.
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory())
.setAudience(Arrays.asList(CRLConstants.IOS_CLIENT_ID, CRLConstants.ANDROID_CLIENT_ID_RELEASE, CRLConstants.ANDROID_CLIENT_ID_DEBUG))
.setIssuer("accounts.google.com")
.build();

Android - Retrofit 2 - Authenticator Result

I'm trying to use Retrofit (2.0.0-beta3), but when using an Authenticator to add a token, I can't seem to get the data from the synchronous call. Our logging on the back-end just shows a lot of login attempts, but I can't get the data from the body to actually add to the header.
public static class TokenAuthenticator implements Authenticator {
#Override
public Request authenticate(Route route, Response response) throws IOException {
// Refresh your access_token using a synchronous api request
UserService userService = createService(UserService.class);
Call<Session> call = userService.emailLogin(new Credentials("handle", "pass"));
// This call is made correctly, as it shows up on the back-end.
Session body = call.execute().body();
// This line is never hit.
Logger.d("Session token: " + body.token);
// Add new header to rejected request and retry it
return response.request().newBuilder()
.header("Auth-Token", body.token)
.build();
}
}
I'm not exactly too sure on why it's not even printing anything out. Any tips on how to solve this issue would be greatly appreciated, thanks for taking the time to help.
These are the sources I've been reading on how to implement Retrofit.
Using Authenticator:
https://stackoverflow.com/a/31624433/3106174
https://github.com/square/okhttp/wiki/Recipes#handling-authentication
Making synchronous calls with Retrofit 2:
https://futurestud.io/blog/retrofit-synchronous-and-asynchronous-requests
I managed to get a decent solution using the TokenAuthenticator and an Interceptor and thought I'd share the idea as it may help some others.
Adding the 'TokenInterceptor' class that handles adding the token to the header is the token exists, and the 'TokenAuthenticator' class handles the case when there is no token, and we need to generate one.
I'm sure there are some better ways to implement this, but it's a good starting point I think.
public static class TokenAuthenticator implements Authenticator {
#Override
public Request authenticate( Route route, Response response) throws IOException {
...
Session body = call.execute().body();
Logger.d("Session token: " + body.token);
// Storing the token somewhere.
session.token = body.token;
...
}
private static class TokenInterceptor implements Interceptor {
#Override
public Response intercept( Chain chain ) throws IOException {
Request originalRequest = chain.request();
// Nothing to add to intercepted request if:
// a) Authorization value is empty because user is not logged in yet
// b) There is already a header with updated Authorization value
if (authorizationTokenIsEmpty() || alreadyHasAuthorizationHeader(originalRequest)) {
return chain.proceed(originalRequest);
}
// Add authorization header with updated authorization value to intercepted request
Request authorisedRequest = originalRequest.newBuilder()
.header("Auth-Token", session.token )
.build();
return chain.proceed(authorisedRequest);
}
}
Source:
http://lgvalle.xyz/2015/07/27/okhttp-authentication/
I have similar authenticator and it works with 2.0.0-beta2.
If you get lots of login attempts from you Authenticator, I suggest make sure that when you make the synchronous call, you are not using Authenticator with that call.
That could end up in loop, if also your "emailLogin" fails.
Also I would recommend adding loggingInterceptor to see all trafic to server: Logging with Retrofit 2
I know it's a late answer but for anyone still wondering how to Add / Refresh token with Retrofit 2 Authenticator, here is a working solution:
Note: preferenceHelper is your Preference Manager class where you set/get your shared preferences.
public class AuthenticationHelper implements Authenticator {
private static final String HEADER_AUTHORIZATION = "Authorization";
private static final int REFRESH_TOKEN_FAIL = 403;
private Context context;
AuthenticationHelper(#ApplicationContext Context context) {
this.context = context;
}
#Override
public Request authenticate(#NonNull Route route, #NonNull Response response) throws IOException {
// We need to have a token in order to refresh it.
String token = preferencesHelper.getAccessToken();
if (token == null)
return null;
synchronized (this) {
String newToken = preferencesHelper.getAccessToken();
if (newToken == null)
return null;
// Check if the request made was previously made as an authenticated request.
if (response.request().header(HEADER_AUTHORIZATION) != null) {
// If the token has changed since the request was made, use the new token.
if (!newToken.equals(token)) {
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + newToken)
.build();
}
JsonObject refreshObject = new JsonObject();
refreshObject.addProperty("refreshToken", preferencesHelper.getRefreshToken());
retrofit2.Response<UserToken> tokenResponse = apiService.refreshToken(refreshObject).execute();
if (tokenResponse.isSuccessful()) {
UserToken userToken = tokenResponse.body();
if (userToken == null)
return null;
preferencesHelper.saveAccessToken(userToken.getToken());
preferencesHelper.saveRefreshToken(userToken.getRefreshToken());
// Retry the request with the new token.
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + userToken.getToken())
.build();
} else {
if (tokenResponse.code() == REFRESH_TOKEN_FAIL) {
logoutUser();
}
}
}
}
return null;
}
private void logoutUser() {
// logout user
}
}
Also note:
preferenceHelper and apiService needs to be provided in some way.
This is not an example that will work for all systems and api's but an example in how adding and refreshing the token should be done using Retrofit 2 Authenticator

Categories

Resources