iOS Google SignIn refreshed idToken has missing profile info in backend authentication - ios

I use GoogleSignIn for iOS (GoogleSignIn-iOS), v6.1.0, in my iOS app.
All calls to my backend have the idToken in the request header.
The id token is verified in the backend. Here I also need to retrieve the users email and name.
(see also: https://developers.google.com/identity/sign-in/ios/backend-auth)
After a new SignIn with GIDSignIn.sharedInstance.signIn everything works fine.
GIDSignIn.sharedInstance.currentUser.profile contains email and name.
When sending the idToken to the backend, the Verifier gives me name and email in its payload, too.
Before I do a backend request, I get a valid (=not expired) idToken, with the following code:
private static func refreshToken(_ authentication: GIDAuthentication) async throws -> GIDAuthentication {
try await withCheckedThrowingContinuation { continuation in
authentication.do { authentication, error in
if let authentication = authentication {
continuation.resume(returning: authentication)
} else if let error = error {
Log.warn("Google SignIn refreshToken failed with -> \(error)")
continuation.resume(throwing: error)
}
}
}
}
I use the following code to get the idToken, before I create the request for my URLSession.
func idToken() async -> String {
do {
guard let user = GIDSignIn.sharedInstance.currentUser else {
Log.error("No GID user to get idToken from")
return ""
}
currentAuth = try await Self.refreshToken(user.authentication) //currentAuth is a class variable
return currentAuth?.idToken ?? ""
} catch {
print("Error during Google SignIn idToken retrieval \(error)")
return ""
}
}
And now my problem comes:
The idToken is refreshed properly. It is valid for another hour, and the verifier in my backend accepts it.
But I can't get the users name from the verified payload data in the backend, the name field is null.
Same happens when I use GIDSignIn.sharedInstance.restorePreviousSignIn (which I call on every app re-start, to do the silent sign in. (But in the app, the values are there in the updated users object profile)
It seems to me, that when the idToken gets refreshed, that it looses the profile scope.
I hope someone can help me with this, or at least explain the behaviour to me.
Thank in advance :)
Update
I checked the idTokens on https://jwt.io.
They are valid, but after the refresh, the jwt payload definitely is missing the profile data, like the users name.
I waited one day and tried again. Now the silent signin after app start gives me a complete idToken with jwt payload including name, but only once. After an hour, when the idToken gets refreshed, the idToken is again missing the profile information

Unfortunately I got no hint here, so I solved my problem as follows.
I hope this approach can save time for some others in the future.
I only require the profile data, when the user logs in to the backend the first time and a new user record is created in the backend.
In all other calls, where I need the JWT for authentication, I only rely on the basic information (ID, email) and handle all other values as optional values.
So I check the users name, if it is available. Otherwise the ID and a valid token is of course sufficent for authentication.

Related

Strange Error: flutter firebase facebook login results empty email and registering _ in firebase console. What to do now?

I have a strange error when I work in firebase flutter facebook login.
It always results in null email. And when I see the firebase console, the email field is registering with _.
I have searched on StackOverflow to figure out this error but could not succeed.
And I have followed the steps in facebook account for iOS setup.
What kind of possible reasons are there?
And If I try to log in with facebook, this shows "You previously logged in to this app with facebook account" even though I never logged in before...
Help me guys. I am struggling with this issue for more than 10 days!!!
// Sign in with Facebook.
static Future<Map<String, dynamic>> signInWithFacebook({bool isSignUp}) async {
try {
await signOutFacebook();
UserCredential userCredential;
// Trigger the sign-in flow
List<String> permissions = ['email', 'public_profile'];
final LoginResult loginResult = await FacebookAuth.instance.login(permissions: permissions);
// Create a credential from the access token
final OAuthCredential facebookAuthCredential =
FacebookAuthProvider.credential(loginResult.accessToken.token);
// Once signed in, return the UserCredential
userCredential = await FirebaseAuth.instance.signInWithCredential(facebookAuthCredential);
final User user = userCredential.user;
print("User info after facebook login ${user.providerData[0].email} ${user.uid}");
// ************************************** This is showing null email ***** //
if (isSignUp) {
Map<String, dynamic> resultOfSaveSocialUserToDatabase = await saveSocialUserToDatabase(user, 'facebook');
return resultOfSaveSocialUserToDatabase;
} else {
Map<String, dynamic> resultOfValidateSocialLogin = await validateSocialLogin(user, 'facebook');
return resultOfValidateSocialLogin;
}
} catch (e) {
print(e);
return {'success': false, 'message': "Sign up with social account failed"};
}
}
This happens cause you might have created your facebook account using your phone number instead of email.
So, it turns out to be an empty email address as an empty identifier.
You can try out with a different fb account which may be created using an email address instead of phone number.
This can also happen when the user elects to not share his email when login in with his FB account for the first time.

AWS Amplify iOS SDK : FederatedSignIn Failed to retrieve authorization token on Amplify.API.post

I've been working with the Amplify SDK to get federatedSignIn working with my iOS app with "Sign in with Apple" and Cognito to eventually make calls to API Gateway / Lambda functions.
TL;DR : My access token does not appear to be "automatically included in outbound requests" to my API as per the last paragraph of this section of the docs : Cognito User pool authorization
I have successfully authenticated using the tutorial found here Authentication Getting Started and other various Youtube videos on the Amazon Web Services channel.
Upon successful sign in through Apple I'm given an ASAuthorizationAppleIDCredential object. This contains the user field (token) which I pass to the Amplify.Auth class using the following Swift code :
func signIn (with userId: String)
{
guard
let plugin = try? Amplify.Auth.getPlugin(for: AWSCognitoAuthPlugin().key),
let authPlugin = plugin as? AWSCognitoAuthPlugin,
case .awsMobileClient (let client) = authPlugin.getEscapeHatch()
else
{
return
}
client.federatedSignIn(providerName: AuthProvider.signInWithApple.rawValue, token: userId) { (state, error) in
if let unwrappedError = error
{
print (unwrappedError)
}
else if let unwrappedState = state
{
print ("Successful federated sign in:", unwrappedState)
}
}
}
All appears to be successful and to double check I use the following bit of code to ensure I'm authorized :
func getCredentialsState (for userId:String)
{
let provider = ASAuthorizationAppleIDProvider()
provider.getCredentialState(forUserID: userId) { (credentialsState, error) in
if let unwrappedError = error
{
print (unwrappedError)
}
switch credentialsState
{
case .authorized:
print ("User Authorized")
case .notFound, .revoked:
print ("User Unauthenticated")
case .transferred:
print ("User Needs Transfer")
#unknown default:
print ("User Handle new use cases")
}
}
}
In the console I see "User Authorized" so everything appears to be working well.
However when I then go to make a call to Amplify.API.post I get the following error:
[Amplify] AWSMobileClient Event listener - signedOutFederatedTokensInvalid
Failed APIError: Failed to retrieve authorization token.
Caused by:
AuthError: Session expired could not fetch cognito tokens
Recovery suggestion: Invoke Auth.signIn to re-authenticate the user
My function for doing the POST is as follows :
func postTest ()
{
let message = #"{'message": "my Test"}"#
let request = RESTRequest (path: "/test", body: message.data(using: .utf8))
Amplify.API.post (request:request)
{
result in switch result
{
case .success(let data):
let str = String (decoding: data, as: UTF8.self)
print ("Success \(str)")
case .failure(let apiError):
print ("Failed", apiError)
}
}
}`
I then went into the API Gateway UI and changed the generated Method Request on my resource from AWS IAM to my Cognito User Pool Authorizer thinking this was the issue. I also changed the awsAPIPlugin authorizationType to "AMAZON_COGNITO_USER_POOLS" in my amplifyconfiguration.json file. This unfortunately did not have any affect.
I've seen posts such as this issue User is not created in Cognito User pool for users logging in with Google federated login #1937 where people discuss the problem of having to to use a web ui to bring up the social sign in. I understand that Apple will reject your app sometimes for this. Therefore this is not a solution.
I then found this post which seems to resolve the issue however this appears to use the old version of the SDK? Get JWT Token using federatedSignIn #1276
I'm not great with Swift (I'm still an Objective C expert, but am slowly learning Swift) so I'm uncertain which path to go here and whether this is actually a solution? It does seem to be quite more complicated than the function I have that does my POST? The RESTRequest does seem to be a simple and easy solution but I'm uncertain how to pass it the Authorization token (or even how to get the token if it is needed here).
However, everything I've read about the SDK is that the authorization should be handled automatically in the background according the docs in my first link above. Specifically pointed out, again, here : Cognito User pool authorization. The last paragraph here states 👍
With this configuration, your access token will automatically be included in outbound requests to your API, as an Authorization header.
Therefore, what am I missing here as this does not appear to automatically include my access token to my outbound requests to my API?

iOS Firebase OTP verification without Sign Up

I need help verifying the OTP with Firebase.
I Managed to receive a SMS with the OTP but when I verify it I get automatically signed up and I only know if the OTP was valid if I signed up - else I get a popup like "invalid otp".
How can I manually validate the otp? My goal is to open another screen where the user puts in more information.
func verifyCode(){
let credential = PhoneAuthProvider.provider().credential(withVerificationID: self.CODE, verificationCode: code)
print(credential)
loading = true
//here i just want to verify my OTP without signing in...
Auth.auth().signIn(with: credential) { (result, err) in //here i am signing in...
self.loading = false
if let error = err{
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.error)
self.code = ""
self.errorMsg = error.localizedDescription
withAnimation{ self.error.toggle()}
return
}
self.gotoRegistration = true
withAnimation{self.status = true}
}
}
There is no way to use Firebase Authentication's phone/OTP provider without automatically signing the user in.
But the fact that the user is signed in to Firebase, does not mean that you have to grant them access to all parts/data in your app. If you want them to provide more information, you can do so before or after signing them in to Firebase, and make it part of the same sign-up flow as far as the user is concerned.
So something like:
// Sign the user in with Firebase
// Check if the user has provider the additional registration information
// If not, send them to the registration information screen
// If so, send them to the next screen of the app
You can also enforce these rules in your back-end code, or (if you use one of Firebase's back-end services) in the server-side security rules.

(iOS) Firestore document snapshotListener stops working 1 hour after signing in

I implemented firebase/auth and use that to sign into firebase using a custom token that I get from our API.
Auth.auth().signIn(withCustomToken: result.data.token) { (authResult, error) in
completion(authResult != nil && error == nil)
}
Then I subscribe to my document using a snapshotListener.
guard let user = Auth.auth().currentUser else {
return
}
listListener = firestoreDB.collection(shoppingListCollectionName).document(user.uid).addSnapshotListener { (documentSnapshot, _) in ....
The listener works for roughly 1 hour and then it stops working. In the logs I get:
Stream error: 'Unauthenticated: Missing or invalid authentication.'
And then I get spammed with:
Stream error: 'Unknown: An internal error has occurred, print and inspect the error details for more information.'
I don't know much the about custom token as you are using custom token to authenticate. But if we are coming to the firebase authentication, the id token issued by firebase has a lifespan of a maximum of one hour. After that the firebase will issue a new id token using the refresh token. I think your id token is getting expired and it is not getting issued again.
The Firebase ID tokens last for only one hour. As you are using a custom token, if you want stay authenticated beyond one hour you will need to use the Firebase Auth REST API. So you will have to make an HTTP request to get an ID token and a refresh token from your custom token (check the Exchange custom token for an ID and refresh token section). Then, you will just have to refresh the ID token every time it expires (check the Exchange a refresh token for an ID token section).

iCloud Cloudkit CKFetchWebAuthTokenOperation

My iOS app checks the iCloud account status and then requests an iCloud WebToken using the following method:
#objc static func fetchWebAuthToken ( _ apiToken : String, _ callback : #escaping CCallbackFunctionWithBoolAndString )
{
let fetchAuthorization = CKFetchWebAuthTokenOperation(apiToken: apiToken)
fetchAuthorization.fetchWebAuthTokenCompletionBlock = { webToken, error in
guard let webToken = webToken, error == nil else {
callback ( false, "[SWIFT] fetchWebAuthToken() error. " + (error?.localizedDescription ?? ""));
return;
}
let encodedWebToken = token.addingPercentEncoding (
withAllowedCharacters: CharacterSet(charactersIn: "+/=").inverted
) ?? token
callback ( true, encodedWebToken );
return;
}
CKContainer.default().privateCloudDatabase.add(fetchAuthorization);
}
Everything works correctly and a properly formatted web token is returned.
I then take that web token and, using Postman, I form the request (with exact values removed):
https://api.apple-cloudkit.com/database/1/iCloud.com.[my container]/development/private/users/caller?ckAPIToken=[development container token]&ckWebAuthToken=[web token]
The response is:
{
"uuid": "[abc]",
"serverErrorCode": "ACCESS_DENIED",
"reason": "private db access disabled for this account"
}
If I request to the public database instead, I get a valid and correct response:
https://api.apple-cloudkit.com/database/1/iCloud.com.[my container]/development/public/users/caller?ckAPIToken=[development container token]&ckWebAuthToken=[web token]
{
"userRecordName": "_[user id]",
"nameComponents": {
"givenName": "[First Name]",
"familyName": "[Surname]"
}
}
So, there's two questions here.
1) If I'm requesting a web token in code for the private database, why is it only allowing me to interact with the public database? It feels like it's providing a web token that's only valid for the public database, regardless of the database I add the action to.
2) What are the security implications of validating a user against the public database like this? The token should expire in 30 minutes, which helps from that front.
To prove that a web token works against the private database, I updated "Sign In Callback" int the Dashboard, copied the resulting ckWebAuthToken and was able to get access to the private database through PostMan, so there's no issue from that end. It seems as if the issue lies entirely with the web token returned from the iOS code.
My guess is that it's because the Users record type in CloudKit is always stored in the public database in every CloudKit container.
There shouldn't be any security risks with this validation against the public databse. In my opinion, Apple shouldn't have ever named it "public" because it's not really public. It's just generally available to the users of the app, but only the application and authenticated users can transact with the database as defined by the developer. It's not available to the public.
I'm going to assume you are doing something fancy with this authentication flow, since authenticating a user on an iOS device doesn't require passing around the ckWebAuthToken. :)

Resources