I have built a SPA that works in the browser in that on load it provides an auth option, I click google signin and the firebase auth flow continues until I have an auth token etc.
I can then switch the PWA and use as normal. But if I then logout, I cannot log back in again with the App using Google Auth - whether I let the google signin popup run in the App or in Chrome, no confirmation ever gets abck to the App and indeed it seems to crash.
The issue has something to do with the loading of the additional tab for the google signin. According to the onscreen dialog Android asks me whether to open this tab in the PWA or in Chrome. Whichever option I pick, the flow does not complete (and because of the disconnection, I can't see anything useful in devtools).
The only flow that seems to work is to continue the login on chrome and, only when that has completed, switch to the App version. That's fine for me to write on StackOverflow but very complicated for my users.
How can I begin to debug this situation:
- should it be possible to do firebase auth from a PWA; and/or
- is there someway to delay the Android popup to add to home screen to til after the user has logged in on the browser?
Happy to share code, and this is the googlesignin function - it doesn't do anything because I wait for the onAuthState message in my code usually and that has all the information I need.
function signinGoogle() {
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function(result) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
}
Per #jasan 's request, I did find a solution based on #bojeil 's comment
function signinGoogle(cb) {
var provider = new firebase.auth.GoogleAuthProvider();
// firebase.auth().signInWithPopup(provider).then(function(result) {
firebase.auth().signInWithRedirect(provider).then(function(result) {
console.log("Google signin successful")
// This gives you a Google Access Token. You can use it to access the Google API.
// var token = result.credential.accessToken;
// Send user to rest of program
cb(token)
})
.catch(function(error) {
logger(error);
});
}
Related
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.
I am implementing sign in with Apple. I can successfully see the Apple login page. I key in the correct credentials. It should be able to sign in/sign up to the firebase based on the returned value from Apple.
However I am getting this error Error: The supplied auth credential is malformed, has expired or is not currently supported. Something must be wrong at the firebase side? You may refer to the onPressAppleLogin function below on the logic. Many thanks!
What I have done:
In Firebase
Authentication with Sign-in provider Apple enabled
My service id is co.myexampleapp.signinwithapple
My authorization callback is https://my-example-app.firebaseapp.com/__/auth/handler
In developer.apple.com
I created a service id co.myexampleapp.signinwithapple with the service Sign In with Apple enabled
I added my-example-app.firebaseapp.com for the Domain and https://my-example-app.firebaseapp.com/__/auth/handler in the Return URLs
My React Native source code
import { appleAuthAndroid } from '#invertase/react-native-apple-authentication';
import firebase from 'react-native-firebase'
getRandomString = (length: any) => {
let randomChars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < length; i++) {
result += randomChars.charAt(Math.floor(Math.random() * randomChars.length))
}
return result
}
onPressAppleLogin = async () => {
const rawNonce = this.getRandomString(20);
const state = this.getRandomString(20)
appleAuthAndroid.configure({
clientId: 'co.myexampleapp.signinwithapple',
redirectUri: 'https://my-example-app.firebaseapp.com/__/auth/handler',
responseType: appleAuthAndroid.ResponseType.ALL,
scope: appleAuthAndroid.Scope.ALL,
nonce: rawNonce,
state,
});
const response = await appleAuthAndroid.signIn();
const appleCredential = await firebase.auth.AppleAuthProvider.credential(response.id_token, rawNonce)
const appleUserCredential = await firebase.auth().signInWithCredential(appleCredential) // error happens here!
}
This is 100% due to the wrong Services ID on the server or the client.
I was working on a project in which we have Django as the backend server the backend developer used a different Services ID on the server & I on the client-side used a different Services ID.
How we solved this issue.
Open the Firebase console in the general settings check the bundle ID of the ios app compare it with the bundle ID in your Xcode. Make sure the Services is ID is correct and you have the latest provisioning profile with the Services Id added inside it.
Read this article to understand how to create a service ID. https://firebase.google.com/docs/auth/android/apple?authuser=4
You need to add the same services ID that you created above in the firebase console where you enable apple auth service in the authentication section
In your case you need to add service id
co.myexampleapp.signinwithapple
in the input box that is shown in the screenshot.
There is some sort of error in initializing the credentials.
Three types of errors may occur:
In the response, the token may be get expired. In that time you can use refresh token function to get new token.
Have a look at the rules in the firebase, if you initialized your app in the locked mode read and write will be set to false. If it so, change it to true.
Check whether you have enabled Api keys.
Important Check whether you enabled the third party access for the Apple Id.
Since the problem is with token, I suggest you to check the following.
Make sure you provided your email in support email in firebase project settings.
Try logging out before performing signing in operation. Due to improper logout during development this may happen.
Make sure you always logout before signin. Helped me in some cases.
Device time - since the token generated will be based on timestamp.
I'm working on Firebase + PhoneGap. I have made a simple Google Auth login system, it's working perfect on web (smart-media-compaing.firebaseapp.com), but when I make it's APK for Android by PhoneGap, the PhoneGap app(Android) doesn't redirect me to Google Auth page.
Because it wont find any localserver/other server address to work on.
How do I redirect to Google Auth in PhoneGap (Android) app?
var provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
'login_hint': 'user#example.com'
});
firebase.auth().signInWithRedirect(provider);
firebase.auth().getRedirectResult().then(function(result) {
if (result.credential) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// ...
var user = result.user;
window.location.replace('profile.html');
}
// The signed-in user info.
var user = result.user;
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
Is it a Firebase problem or a PhoneGap problem?
Try to use the InAppBrowser plugin to open the external website.
https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/
Phonegap app runs with file:// protocol, and HTTP requests always go through the proxy server, maybe the proxy server can't process the auth request successfully.
I am trying to set up a proper facebook login in my react native app. For now I have it working in a pure webview with oauth login to my server but I want to use the native facebook login to be able to take advantage of the facebook app.
I am using the following libraries:
Server side
https://github.com/FriendsOfSymfony/FOSUserBundle
https://github.com/hwi/HWIOAuthBundle
https://github.com/FriendsOfSymfony/FOSOAuthServerBundle
App side
https://github.com/magus/react-native-facebook-login
So my facebook login is perfectly functional on my website as well as on my app in a webview for OAuth by calling /oauth/v2/auth in a webview and handling the token.
But it is kinda messy because in the webview you have to type your full email + password combo.
So right now I'm just getting an authorization error when calling /login/facebook-check in a webview on the Login success event (by the native plugin), I could use some help to finish this.
Finally made it work. The hack was to use all the existing services to work together.
I made a custom controller. Some security checks are needed but still this works:
/**
* #Route("/api/facebook-connect/{accessToken}", name="api_facebook_connect", defaults={"_format" = "json"})
* #Rest\View()
*/
public function facebookLoginAction($accessToken, Request $request)
{
$token = new OAuthToken($accessToken);
$token->setResourceOwnerName('facebook');
$oauthUserProvider = $this->get('app.oauth.provider.user_provider');
$ressourceOwnerMap = $this->get('hwi_oauth.resource_ownermap.main');
$userChecker = new UserChecker();
$oauthProvider = new OAuthProvider($oauthUserProvider, $ressourceOwnerMap, $userChecker);
$token = $oauthProvider->authenticate($token);
$this->get('security.token_storage')->setToken($token);
$client = $this->get('doctrine.orm.entity_manager')->getRepository('AppBundle:Client')->findOneBy([], ['id' => 'DESC']);
$oauth2server = $this->get('fos_oauth_server.server');
$accessToken = $oauth2server->createAccessToken($client, $this->getUser(), 'user', 3600);
return $accessToken;
}
Will update this as I clean this up.
I'm not sure about how to properly handle the server-side part, however here are a few details about how we integrated Facebook login in one of our app:
We first started by using https://github.com/magus/react-native-facebook-login but later switched to https://github.com/facebook/react-native-fbsdk, which is maintained by Facebook and allow to access to other Facebook services (in particular, we used the Share API)
In both case (react-native-fbsdk or not), the flow was like this:
We have a Connect with Facebook button (a normal button, nothing fancy - we're not using the one provided by the modules).
When clicked, we call the Login method with the appropriate permissions. This should work out of the box, meaning that you'll have either a webview displayed (if you don't have the Facebook app) or the native Facebook app shown.
If the user declines the login to the app, nothing will happen.
If the user accepts, the app will receive the Access Token that can be used to issue calls to the Facebook API on the behalf of the user. This looks like this using react-native-fbsdk:
// This can be put in a Facebook login button component or a service,
// and should be called when the user wants to connect with Facebook
LoginManager.logInWithReadPermissions(permissions).then((result) => {
if (result.isCancelled) {
this.props.onCancel();
return;
}
AccessToken.getCurrentAccessToken().then((data) => {
this.props.onLogin(data);
});
}, (error) => {
console.warn('Facebook Error', error);
});
We then send the access token to our server which is able to fetch the profile of the user via the Facebook Graph API, creating a user account on the server if needed (i.e: if it's the first time the user log on).
// Called once we got the access token from the data in the previous
// step (this.props.onLogin(data)).
loginWithFacebook(facebookAccessToken) {
return fetch(`${apiHost}/api/login/facebook`, {
method: 'GET',
headers: {
// We pass the access token using the Authorization header:
Authorization: `Bearer ${facebookAccessToken}`,
},
}).then(() => {
// Whatever, for example get the user info returned by the server
// and store them.
});
}
On the server, we get the Access Token from the headers and use it to get the user profile (populating the account of the user for our app, for example with his avatar and name) and associate the user account with the facebook id of the user.
If the user have the Facebook app and already accepted the app, nothing will be asked to him the next time he tries to log in. You just click on the login button and get logged to the app :)
If the user don't have the Facebook app, the Facebook SDK always display a webview with the login page.
Hope this can help!
Question says it all. In Firebase, how do I confirm email when a user creates an account, or, for that matter, do password reset via email.
I could ask more broadly: is there any way to send emails out from Firebase? E.g. notifications, etc. This isn't the kind of thing you would usually do client-side.
Update
Note that this was never a very secure way of handling email verification, and since Firebase now supports email verification, it should probably be used instead.
Original answer
I solved the email verification using the password reset feature.
On account creation I give the user a temporary (randomly generated) password. I then trigger a password reset which will send an email to the user with a link. The link will allow the user to set a new password.
To generate a random password you can use code similar to this:
function () {
var possibleChars = ['abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?_-'];
var password = '';
for(var i = 0; i < 16; i += 1) {
password += possibleChars[Math.floor(Math.random() * possibleChars.length)];
}
return password;
}
Note that this is happening on the client, so a malicious user could tamper with your logic.
This would need to be done outside of firebase. I store users at /users/ and keep a status on them (PENDING, ACTIVE, DELETED). I have a small service that monitors users of a PENDING status and sends out a confirmation email. Which has a link to a webservice I've created to update the user status to ACTIVE.
[Engineer at Firebase - Update 2014-01-27]
Firebase Simple Login now supports password resets for email / password authentication.
Each of the Simple Login client libraries has been given a new method for generating password reset emails for the specified email address - sendPasswordResetEmail() on the Web and Android, and sendPasswordResetForEmail() on iOS.
This e-mail will contain a temporary token that the user may use to log into their account and update their credentials. This token will expire after 24 hours or when the user changes their password, whichever occurs first.
Also note that Firebase Simple Login enables full configuration of the email template as well as the sending address (including whitelabel email from your domain for paid accounts).
To get access to this feature, you'll need to update your client library to a version of v1.2.0 or greater. To grab the latest version, check out https://www.firebase.com/docs/downloads.html.
Also, check out https://www.firebase.com/docs/security/simple-login-email-password.html for the latest Firebase Simple Login - Web Client docs.
As at 2016 July, you might not have to use the reset link etc. Just use the sendEmailVerification() and applyActionCode functions:
In short, below is basically how you'll approach this, in AngularJS:
// thecontroller.js
$scope.sendVerifyEmail = function() {
console.log('Email sent, whaaaaam!');
currentAuth.sendEmailVerification();
}
// where currentAuth came from something like this:
// routerconfig
....
templateUrl: 'bla.html',
resolve: {
currentAuth:['Auth', function(Auth) {
return Auth.$requireSignIn() // this throws an AUTH_REQUIRED broadcast
}]
}
...
// intercept the broadcast like so if you want:
....
$rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) {
if (error === "AUTH_REQUIRED") {
$state.go('login', { toWhere: toState });
}
});
....
// So user receives the email. How do you process the `oobCode` that returns?
// You may do something like this:
// catch the url with its mode and oobCode
.state('emailVerify', {
url: '/verify-email?mode&oobCode',
templateUrl: 'auth/verify-email.html',
controller: 'emailVerifyController',
resolve: {
currentAuth:['Auth', function(Auth) {
return Auth.$requireSignIn()
}]
}
})
// Then digest like so where each term is what they sound like:
.controller('emailVerifyController', ['$scope', '$stateParams', 'currentAuth', 'DatabaseRef',
function($scope, $stateParams, currentAuth, DatabaseRef) {
console.log(currentAuth);
$scope.doVerify = function() {
firebase.auth()
.applyActionCode($stateParams.oobCode)
.then(function(data) {
// change emailVerified for logged in User
console.log('Verification happened');
})
.catch(function(error) {
$scope.error = error.message;
console.log(error.message, error.reason)
})
};
}
])
And ooh, with the above approach, I do not think there's any need keeping the verification of your user's email in your user data area. The applyActionCode changes the emailVerified to true from false.
Email verification is important when users sign in with the local account. However, for many social authentications, the incoming emailVerified will be true already.
Explained more in the article Email Verification with Firebase 3.0 SDK
What I did to work around this was use Zapier which has a built in API for firebase. It checks a location for added child elements. Then it takes the mail address and a verification url from the data of new nodes and sends them forwards. The url points back to my angular app, which sets the user email as verified.
As I host my app files in firebase, I don't need have to take care of any servers or processes doing polling in the background.
There is a delay, but as I don't block users before verifying mails it's ok. Zapier has a free tier and since I don't have much traffic it's a decent workaround for time being.
The new Firebase SDK v3 appears to support email address verification, see here (put your own project id in the link) but it doesn't appear to be documented yet.
I have asked the question on SO here
See #SamQuayle's answer there with this link to the official docs.
As noted by various others Firebase does now support account related emails but even better, as of 10 days ago or so it also supports sending any kind of email via Firebase Functions. Lots of details in the docs and example code here.
I used following code to check the email verification after creating new account.
let firAuth = FIRAuth.auth()
firAuth?.addAuthStateDidChangeListener { auth, user in
if let loggedUser = user {
if loggedUser.emailVerified == false {
loggedUser.sendEmailVerificationWithCompletion({ (error) in
print("error:\(error)")
})
}
else {
print(loggedUser.email)
}
} else {
// No user is signed in.
print("No user is signed in.")
}
}
I used MandrillApp. You can create an API key that only allows sending of a template. This way even thought your key is exposed it can't really be abused unless someone wants to fire off tonnes of welcome emails for you.
That was a hack to get myself off the ground. I'm now enabling CORS from a EC2 that uses the token to verify that the user exists before extending them a welcome via SES.