I have been using the SafetyNet reCAPTCHA for quite some time. Worked perfectly but since SafetyNet is deprecated, I decided to migrate my integration to a reCAPTCHA Enterprise.
SafetyNet reCAPTCHA was pretty straightforward:
val recaptcha = SafetyNet.getClient(this).verifyWithRecaptcha(BuildConfig.RECAPTCHA_SITE)
recaptcha.addOnSuccessListener(this) { response ->
val token = response.tokenResult
// verify token with https://www.google.com/recaptcha/api/siteverify
}
Making this SafetyNet call, I would get a CAPTCHA loading dialog, and then if the user seems a bot, the usual CAPTCHA with images would appear.
Now, for the reCAPTCHA Enterprise, I am getting a completely different result:
suspend fun validateUser(action: String) {
val recaptcha = Recaptcha.getClient(activity.application, BuildConfig.RECAPTCHA_SITE)
recaptcha.onSuccess { client ->
client.getUserToken(action)
}
}
private suspend fun RecaptchaClient.getUserToken(action: String) {
val execution = execute(RecaptchaAction.custom(action))
execution.onSuccess { token ->
// create assessment
}
}
I can get the client, and can get the token. Now, I need to create an assessment to get the score, but shouldn't a CAPTCHA dialog or something appears when I call the execute(RecaptchaAction.custom(action)) method? Does reCAPTCHA Enterprise only work as an invisible CAPTCHA?
I feel that I am missing something from the documentation, but I really can't seem to understand how this new reCAPTCHA works on Android. Does any one have any experience with it?
It depends which type of reCAPTCHA Enterprise key you created
https://cloud.google.com/recaptcha-enterprise/docs/choose-key-type#differences-keys
If you created a score-based key (recommended) there is no visual challenge, you will get a score back from your assessment between 0.0 and 1.0.
If you created a checkbox key the end user will click a checkbox, and then (possibly) be presented with a CAPTCHA challenge depending if the traffic appears suspicious.
Note that you cannot use checkbox keys if you use the reCAPTCHA Enterprise Android SDK, in this case you can only use score-based keys
https://cloud.google.com/recaptcha-enterprise/docs/instrument-android-apps
Related
I'm integrating PayPal in my Android app. I have successfully done the part where we can redirect to Paypal Activity by setting price, currency, and PayPalPaymentIntent. What I'm looking for is not available anywhere. I want to make Paypal transaction by using the access token. I'm getting token from the server-side. My client do not want to set the price from app side so that's why he's sending me the token.
My question is how do I set the token in PayPalPayment instead of price and currency? Below is my existing code.
private fun processPaypalPayment() {
val paypalPayment =
PayPalPayment(BigDecimal("1"), "USD", "Test", PayPalPayment.PAYMENT_INTENT_SALE)
val intent = Intent(this#PackagesActivity, PaymentActivity::class.java)
intent.putExtra(PayPalService.EXTRA_PAYPAL_CONFIGURATION, config)
intent.putExtra(PaymentActivity.EXTRA_PAYMENT, paypalPayment)
startActivityForResult(intent, PAYPAL_REQ_CODE)
}
Access token is the wrong term. What you need from the server is for it to create a PayPal order ID (via the v2/checkout/orders API) and send this id to your app, for the approval flow. After approval you communicate the id back the server so it can capture it, store any resulting successful capture in its database, and immediately return the result of the capture (success or particular error) to the calling app.
Documentation for such a server-side integration with Native Checkout for Android can be found here, basically you need to use the actions .set() with the created id as a parameter
payPalButton.setup(
createOrder = CreateOrder { createOrderActions ->
val orderId: String // Retrieve your order ID from your server-side integration
createOrderActions.set(orderId)
}
)
If this code doesn't look familiar you are probably attempting to use a deprecated android SDK rather than the current Native Checkout one. Don't do that, it's very old and won't be accepted by the app store.
Background
Inside the app, there is a Google-login step to register with the server using a token. This is done via the dependency of :
api('com.google.android.gms:play-services-auth:20.0.0')
The app triggers showing up to 2 dialogs for the user :
Login (not shown if user has logged in for the currently installed app) :
Granting permissions (not shown if granted the permissions in the past):
This works fine for most cases.
If the user has already logged in and granted permissions, we can use the token that we got last time, assuming it's not expired. I check if it's expired using:
GoogleSignIn.getLastSignedInAccount(context)?.isExpired`) .
The problem
I've found a special problematic scenario:
User has logged-in in the past and granted some permissions
User went to Google-account-manager and revoked the access to the app (here).
User tried to login again (for example after removal of the app)
On this case, I get a bad token that can't be used. It's actually the exact same token that I got from before revoking the access. It's probably using a cached token from last time, to avoid un-needed communication with Google server.
In this case, the server (of the SDK I work on) will send me an error that this token is invalid (which is correct), as it tries to use it.
This is problematic and seems to me like a bug on Google's SDK (I've reported here), because the token is supposed to work, as the user has re-logged in using the login-dialog, as everything was reset.
What I've tried
I tried to use various API functions, but none of them seem to tell me if the token is valid, or let me request a new token for login dialog in case the current one is invalid.
The only workaround for this that I've found, is that after a single login-dialog, and detecting that there is an error with the token (got it via the server), I choose to logout and re-login entirely:
#WorkerThread
fun logout(context: Context, googleClientId: String) {
val options =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestServerAuthCode(googleClientId)
.requestEmail()
.build()
val signInClient = GoogleSignIn.getClient(context, options)
val lastSignedInAccount = GoogleSignIn.getLastSignedInAccount(context)
if (lastSignedInAccount != null) {
Tasks.await(signInClient.revokeAccess())
Tasks.await(signInClient.signOut())
}
}
Only after that, I can login using an Intent that I prepare:
#WorkerThread
fun prepareIntent(context: Context, googleClientId: String): Intent {
val options =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestServerAuthCode(googleClientId)
.requestEmail()
.build()
val signInClient = GoogleSignIn.getClient(context, options)
val lastSignedInAccount = GoogleSignIn.getLastSignedInAccount(context)
if (lastSignedInAccount?.isExpired == true) {
var success = false
kotlin.runCatching {
val result: GoogleSignInAccount? = Tasks.await(signInClient.silentSignIn())
success = result?.isExpired == false
}
if (!success)
kotlin.runCatching {
Tasks.await(signInClient.revokeAccess())
Tasks.await(signInClient.signOut())
}
}
return signInClient.signInIntent
}
This isn't a nice thing to do, because the user sees 3 dialogs instead of up to just 2 dialogs as I've shown in the beginning :
Login
Grant permissions
Login again, as the token was invalid.
The questions
How can I avoid 2 login-dialogs ?
Is there an API that forces getting a new token for login dialog?
Is this a known bug, and this is the only workaround I can indeed use?
It is not a bug - this behavior is intentional and you will have to handle it properly.
According to the google official user consent policy if your users have revoked access to some of the features you will have to ask them once more.
What does it mean in terms of UI/UX of your app? Well, it depends. If the features you need to access are on some separate rarely accessible pages - you can force users to relogin only when they enter those pages. If your entire app depends on those features - you would have to force relogin users on your apps enter.
The important thing to understand is that you will have to relogin users and ask them once more for their consent, no matter what - there is no other way. You cannot get a new token silently if the consent was revoked. It is an intentional limitation.
Regarding two dialogs - you cannot avoid it if some of the scopes are consensual.
Regarding the actual implementation, there are several ways.
The easiest way is to wait for the access error for the requests with the invalid tokens and on error relogin user.
The harder but more user/developer friendly method is to check the authorized scopes of your token via https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=YOUR_TOKEN(I am not sure if the SDK has this function, also there are v1 and v3 versions of this request.) which will give you the response something like
{ "audience":"", "user_id":"", "scope":"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", "expires_in":0 }
where the scope field is a space-separated list of current token scopes. If there is no scope you need for your current feature - just relogin the user with the needed scope.
Of course, the approach only works for the scoped access rights for the generic ones(as background location disclosure or specific analytics collection) you will have to wait for the error.
Apparently the IOS version of this library AppAuth supports logging out using the end_session_endpoint but with the android version you are forced to implement one or find an alternative way of logging the user out.
Currently using the kotlin version of the library
on authorisationRequestBuilder if setPrompt method is not called, then during login you get the KMSI (keep me signed in) checkbox option.
Invalidating authState, persisted tokens and all other resources will not have an effect because hitting the authorise endpoint once again will automatically issue another token. This is because technically they are still logged in the server.
Using only the end_session_endpoint and no postLogoutRedirectUri provided. Is it possible to successfully log the user out and automatically redirect the user by closing the open chrome custom tabs immediately after the endpoint has been a success?
Currently calling the end_session_endpoint using AppAuth will successfully log the user out (You can see this in the open chrome custom tabs) but doesn't redirect or close the custom tabs.
How can this be achieved. I have configured the manifest with LogoutRedirectUriReceiverActivity but not sure how to initiate this. Other examples rely on the postLogoutRedirectUri being present.
Currently postLogoutRedirectUri doesn't exist on the discoveryDoc.
using IdentityServer4 you can include the idToken in the end session request. This way the user is logged out automatically without consent screens and such. Consequently you do not need a redirect after logout.
Your request would look like this:
GET /connect/endsession?id_token_hint=
source: https://identityserver4.readthedocs.io/en/latest/endpoints/endsession.html
Is it clear that the end_session_endpoint is a call to the server to logout? This sample code includes a callback (after end_session_endpoint) that removes any auth tokens stored within the app.
/*
* Do an OpenID Connect end session redirect and remove the SSO cookie
*/
fun getEndSessionRedirectIntent(metadata: AuthorizationServiceConfiguration,
idToken: String?): Intent {
val extraParams = mutableMapOf<String, String>()
val request = EndSessionRequest.Builder(metadata)
.setIdTokenHint(idToken)
.setPostLogoutRedirectUri(this.config.getPostLogoutRedirectUri())
.setAdditionalParameters(extraParams)
.build()
return authService.getEndSessionRequestIntent(request)
}
So, I've implemented Firebase Auth & AuthUI and successfully used the Google auth provider to log in a user.
However, now I'm trying to access the Google APIs but I'm getting a login error when I try to access them. Code below is attempting to connect to Google Fit.
val account = GoogleSignIn.getAccountForExtension(requireContext(), fitnessOptions)
Fitness.getHistoryClient(requireContext(), account)
.readDailyTotal(DataType.TYPE_STEP_COUNT_DELTA)
.addOnSuccessListener { r ->
val first = r.dataPoints.first()
...
}
.addOnFailureListener { e ->
...
}
at which point I'm getting an error message of:
com.google.android.gms.common.api.ApiException: 4: The user must be signed in to make this API call.
I've debugged the Firebase Auth process and can see that there are two providers on the current FirebaseUser, one Google & one Firebase which looks like rather than authing my app with Google, Firebase is authing itself, then authing my app with Firebase.
I'm pretty sure I can remove AuthUI and manually do the Google SSO implementation (per Google's docs) but I really don't want to as I plan on having pretty much all the Auth providers enabled and I don't want to have to code them all manually (not to mention doing all the layout etc)
So, if anyone knows how to access the Google APIs/signed in Google Account from the Firebase authed user, I'd love to hear it!
Thanks in advance for any help you can render!
Ok, I figured it out. Firebase doesn't pass any default scopes to the Google sign in provider. Fixed it by doing
...
AuthUI.IdpConfig.GoogleBuilder()
.setScopes(listOf(Scopes.FITNESS_ACTIVITY_READ)
...
to access the standard steps read information. You can find out which scopes you need here and reverse-engineer it's name in the Scopes class from there.
Hope that helps!
I'm trying to authenticate against an Amazon Cognito Api, however it's not working...
Creating a CognitoUserPool for registering and signing in works. But how to proceed form here on?
In onSuccess(cognitoUserSession: CognitoUserSession) gives me a session, from which I can get a jwtToken (userSession.accessToken.jwtToken).
How to use the session in combination with the ApiFactor?
val api: DevetrackcommercialplaygroundClient = ApiClientFactory()
.apiKey(apiKey)
.build(Client::class.java)
val get= api.getFoo("id") // no auth; works
val post = api.postBar("id", something) // has auth; doesn't work
Always gives me 401. Both, if I set apiKey to the api key and also if I set it to the jwtToken.
So how can I use CognitoUserSession and ApiClientFactory in conjunction?
Seems like ApiGateway is not really meant to be used with user pools as the whole requests would needs to be created manually without any help from the sdk - same goes for the response.
See https://github.com/aws-amplify/aws-sdk-android/issues/767
Maybe this will change at some point in the future:
I agree with you that the Auth integration with API Gateway is not developer friendly and easy to use. We will take this feedback to the team to investigate on how to improve this integration. - kvasukib