Flutter: how do I get Apple credential to link an apple account to an account on my firebase app? - flutter

The Official Firebase docs say:
...
3.Get a Credential object for the new authentication provider:
// Google Sign-in
final credential = GoogleAuthProvider.credential(idToken: idToken);
// Email and password sign-in
final credential =
EmailAuthProvider.credential(email: emailAddress, password: password);
// Etc.
4.Pass the Credential object to the sign-in user's linkWithCredential() method:
try {
final userCredential = await FirebaseAuth.instance.currentUser
?.linkWithCredential(credential);
} on FirebaseAuthException catch (e) {
// ...
}
I have not found any way to get a credential (more specifically, an AuthCredential) for Apple, which is needed as an argument to the function:
FirebaseAuth.instance.currentUser.linkWithCredential(AuthCredential credential)
I have found ways to link Google and Facebook accounts to existing accounts on my firebase app by following the above described methods, just couldn't figure out a way to get that credential object for Apple. Here's what I tried:
For Google, there is:
final credential = GoogleAuthProvider.credential(...);
For Facebook, there is:
final credential = FacebookAuthProvider.credential(...);
I could not find this in any docs, I checked, and yes, for Apple there is a similar function:
String accessToken = ? //how do I get this???
final credential = AppleAuthProvider.credential(accessToken);
The problem is, how do I get this accessToken String, which is required as a parameter? I have searched everywhere, and couldn't find a solution.
The only possibility I see now to link an apple account to an existing account on my app, is to:
call the same code that I use for Signing in With Apple
and warn the user that he should use "Show My Email" and NOT "Hide My Email", so that this "Sign in With Apple" gets added to the existing firebase account (important: I link accounts that have the same email in my firebase auth settings).
Because if the user chooses "Hide My Email" instead, a new firebase account will be created with the randomly generated email address (for anonymity) from Apple.

I have used the "the_apple_sign_in" package before. In this package:
You can get the access token from the credantial as String.fromCharCodes(appleIdCredential.authorizationCode!).
You can get the id token from the credantial as String.fromCharCodes(appleIdCredential!.identityToken!).
Perhaps you can similarly get the Firebase credential. Also here you can find a sample code with this package:
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<UserModel?> signInWithApple(
{List<Scope> scopes = const [Scope.email, Scope.fullName]}) async {
UserModel userModel;
// 1. perform the sign-in request
final result = await TheAppleSignIn.performRequests(
[AppleIdRequest(requestedScopes: scopes)]);
// 2. check the result
switch (result.status) {
case AuthorizationStatus.authorized:
final appleIdCredential = result.credential;
final oAuthProvider = OAuthProvider("apple.com");
final credential = oAuthProvider.credential(
idToken: String.fromCharCodes(appleIdCredential!.identityToken!),
accessToken:
String.fromCharCodes(appleIdCredential.authorizationCode!),
);
final authResult = await _auth.signInWithCredential(credential);
final firebaseUser = authResult.user;
firebaseUser!.updateDisplayName(
'${appleIdCredential.fullName!.givenName} ${appleIdCredential.fullName!.familyName}');
userModel = UserModel(
uid: firebaseUser.uid,
name: firebaseUser.displayName,
email: firebaseUser.email,
image: firebaseUser.photoURL,
idToken:
String.fromCharCodes(appleIdCredential.authorizationCode!));
return userModel;
case AuthorizationStatus.error:
return null;
case AuthorizationStatus.cancelled:
return null;
}
}

Related

Getting a null user id while accessing the id of current user in firebase flutter?

Actually I'm trying to get current user id after authentication but I don't know how to do it. While registering email/password authentication only stores email, password as well as uid. I tried to fetch that uid by calling following function after pressing login button but it return null. I am not able to get the uid of current user.
Calling a function after pressing login button:
final FirebaseAuth auth = FirebaseAuth.instance;
Future<void> inputData() async {
final User? user = await auth.currentUser;
final uid = user?.uid;
// here you write the codes to input the data into firestore
print("User id: ${uid}");
}
You can see in the console it prints null:
Most likely your inputData function runs before the user is signed in. To ensure your code can properly react to when the user is signed in or out, use an auth state listener as shown in the first snippet in the documentation on getting the current user.
Future<void> inputData() async {
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user != null) {
print(user.uid);
}
});
}
one reason why you might be getting null is you're not awaiting the getToken function, so make it a future and await it, I'm sure you'll get the token provided the user is authenticated.

Getting user´s info with Apple Sign In

I am trying to implement Apple Signin to my Flutter app.
I have used the FlutterFire proposed code for it.
Future<UserCredential> signInWithApple() async {
// To prevent replay attacks with the credential returned from Apple, we
// include a nonce in the credential request. When signing in with
// Firebase, the nonce in the id token returned by Apple, is expected to
// match the sha256 hash of `rawNonce`.
final rawNonce = generateNonce();
final nonce = sha256ofString(rawNonce);
// Request credential for the currently signed in Apple account.
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: nonce,
);
// Create an `OAuthCredential` from the credential returned by Apple.
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
);
// Sign in the user with Firebase. If the nonce we generated earlier does
// not match the nonce in `appleCredential.identityToken`, sign in will fail.
return await FirebaseAuth.instance.signInWithCredential(oauthCredential);
}
Is there a way to get some of the user´s data, like email, name, profile image?
I would like to create a user account for my project using some of the apple user data.
When launching signInWithApple() I am getting the following dialog
After getAppleIDCredential and signInWithCredential are completed, and the user allowed to access the data you requested, these should be accessible from appleCredential. For example after this code is completed:
final rawNonce = generateNonce();
final nonce = sha256ofString(rawNonce);
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: nonce,
);
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
);
final userCredential = await FirebaseAuth.instance.signInWithCredential(oauthCredential);
You can get user data from appleCredential:
appleCredential.familyName
appleCredential.givenName
appleCredential.email
It is good idea to wrap this whole sign in logic into a try / catch block to be able to catch exceptions.

How to increase Firebase Authentication Session Time using Flutter?

I developed an Agro App and I have enabled offline persistence because, after logging in, operators can spend 3-4 hours recording information offline until they return to the central point where there is internet.
The problem I have is that sometimes the application asks for credentials (login and password) again when they do not have internet, which means that they cannot record information offline or they have to go back to the central point to authenticate which is not efficient.
I understand that the Firebase auth token lasts up to an hour, how can I extend this time?
The way I am authenticating users using a provider is like following:
class UsuarioProvider {
final FirebaseAuth _firebaseAuth;
DatabaseReference db = FirebaseDatabase.instance.reference();
UsuarioProvider({FirebaseAuth firebaseAuth})
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
Future <Map<String, dynamic>> signIn(String email, String password) async {
try {
UserCredential result = await FirebaseAuth.instance.signInWithEmailAndPassword(email: email, password: password);
User user = result.user;
final token = await user.getIdToken();
return {'ok' : true, 'token' : token, 'localId': user.uid, 'email' : user.email};
} catch (e) {
print(e);
return {'ok': false, 'code': '${e.code}', 'mensaje': '${e.message}' };
}
}
How can I increase the Firebase Token Time? or How can I prevent users from being prompted for credentials when they are offline?
UPDATE:
The way I'm using to call firebase is:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final prefs = new PreferenciasUsuario();
await prefs.initPrefs();
final pushNotificationsProvider = new PushNotificationsProvider();
pushNotificationsProvider.initNotifications();
FirebaseDatabase database;
database = FirebaseDatabase.instance;
database.setPersistenceEnabled(true);
database.setPersistenceCacheSizeBytes(10000000); // 10MB de cache
runApp(MyApp());
}
Firebase Authentication uses two types of tokens: a refresh token that "never" expires, and a short-lived ID token that is valid for an hour but is auto-refreshed by the SDK. There is no way to expand the life-time of the ID tokens.
If you want to pass a value along to a client that they can use for a longer period of time, you can mint an authentication cookie, which can be valid for up to two weeks. Note that this is a sensitive operation, so it can only be performed in trusted environments (typically by using the Firebase Admin SDK). For a full walkthrough of the process, see the Firebase documentation on Managing Session Cookies.

How to get Firebase UID knowing email user?

I am building a Flutter app where the administrator profile can create users to access their company. The code works right, unless the new user was previously created for another company. In this case an error of type ERROR_EMAIL_ALREADY_IN_USE appears from FIREBASE AUTH. What I want to do is simply retrieve the assigned UID from FIREBASE AUTH, which is necessary to assign the user within my database to an additional company.
It's my code...
_register(LoginBloc bloc, BuildContext context) async{
final usuarioBloc = Provider.usuarioBloc(context);
if (!formKey.currentState.validate() ) return;
final info = await usuarioProvider.crearUsuarioFirebase(bloc.email, bloc.password, true);
if (info['ok']) {
final keyUserId = info['localId'];
usuarioProvider.crearUsuarioRaiz(keyUserId, _prefs.idEmpresa, bloc.email);
usuario.idUsuario = info['localId'];
usuario.correo = bloc.email;
usuarioBloc.crearUsuarioEmpresa(usuario, usuario.idUsuario, usuario.idEmpresa); //to create user in the Company
print('******* User was Created *************');
} else { //info['ok'] is false
switch (info['mensaje'].code) {
case 'ERROR_EMAIL_ALREADY_IN_USE':
usuario.correo = bloc.email;
// usuario.idUsuario = ????????
// Here I would like to retrieve the UID to assign it to their additional Company
usuarioBloc.crearUsuarioEmpresa(usuario, usuario.idUsuario, usuario.idEmpresa); //to create user in the Company
print('*** User already in use, the user can use his/her usual password ***');
break;
default:
print(info['mensaje'].message); //If it was a different error
}
}
}
In Provider, I have...
Future <Map<String, dynamic>> crearUsuarioFirebase(String email, String password, [bool desdeAdmin = false]) async {
try {
AuthResult result = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
FirebaseUser user = result.user;
return {'ok' : true, 'localId':user.uid, 'email' : user.email};
} catch (e) {
print(e);
return {'ok': false, 'mensaje': e};
}
}
How can I programmatically obtain the UID knowing its user email?
There is no way to look up a user's UID from their email address using the Firebase Authentication client-side APIs. Since this lookup is considered a trusted operations, it is only available in the Admin SDK for Firebase Authentication.
The two most common solutions are:
Create a custom server-side API in a trusted environment (such as Cloud Functions) that performs the lookup, and then call that API from your client-side application. You will have to make sure that only authorized users can perform this lookup.
Store the information about each user into a database (like the Realtime Database that you tagged your question with) when their account is created, or whenever they sign in. Then you can look up the UID from the email in the database. Here too, you will have to ensure that the data is only available in ways that fit with your application's data privacy requirements.
Note that if you just need to know whether an email address is in use (and not the specific UID that uses it), you can call the fetchSignInMethodsForEmail method.

Setting custom claims for Firebase auth from flutter

I'm using Firebase auth for an app, but as part of user creation I need to set some custom claims.
I've written a cloud function to set the claims when a user is created:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// On sign up.
exports.processSignUp = functions.auth.user().onCreate(user => {
let customClaims;
// Set custom user claims on this newly created user.
return admin.auth().setCustomUserClaims(user.uid, {
'https://hasura.io/jwt/claims': {
'x-hasura-default-role': 'user',
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.uid
}
})
.then(() => {
// Update real-time database to notify client to force refresh.
const metadataRef = admin.database().ref("metadata/" + user.uid);
// Set the refresh time to the current UTC timestamp.
// This will be captured on the client to force a token refresh.
return metadataRef.set({
refreshTime: new Date().getTime()
});
})
.then(() => {
return admin.auth().getUser(user.uid);
})
.then(userRecord => {
console.log(userRecord);
return userRecord.toJSON();
})
.catch(error => {
console.log(error);
});
});
When I print out to the console the userRecord I can see the custom claims are set correctly.
Then in flutter I get the token from the created user, but it then doesn't seem to have the custom claims attached.
I'm using this code to create the user and print the claims in flutter
Future<FirebaseUser> signUp({String email, String password}) async {
final FirebaseUser user = (await auth.createUserWithEmailAndPassword(
email: email,
password: password,
)).user;
IdTokenResult result = await (user.getIdToken(refresh: true));
print('claims : ${result.claims}');
return user;
}
If I inspect the token itself in a jwt debugger I can see its not got the custom claims on it.
Is it that I need some additional steps to try and get an updated token once the claims have been set?
I've tried user.reload() and user.getIdToken(refresh: true) but they don't seem to help.
Any ideas on how to get the token that has the custom claims?
For future reference, I managed to get this working with Doug's suggestions.
Here's my firebase sdk admin function.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const firestore = admin.firestore();
const settings = {timestampsInSnapshots: true};
firestore.settings(settings);
// On sign up.
exports.processSignUp = functions.auth.user().onCreate(async user => {
// Check if user meets role criteria:
// Your custom logic here: to decide what roles and other `x-hasura-*` should the user get
let customClaims;
// Set custom user claims on this newly created user.
return admin.auth().setCustomUserClaims(user.uid, {
'https://hasura.io/jwt/claims': {
'x-hasura-default-role': 'user',
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.uid
}
})
.then(async () => {
await firestore.collection('users').doc(user.uid).set({
createdAt: admin.firestore.FieldValue.serverTimestamp()
});
})
.catch(error => {
console.log(error);
});
});
Then on the flutter side of things
Future<FirebaseUser> signUp({String email, String password}) async {
final FirebaseUser user = (await auth.createUserWithEmailAndPassword(
email: email,
password: password,
)).user;
currentUser = user;
await waitForCustomClaims();
return user;
}
Future waitForCustomClaims() async {
DocumentReference userDocRef =
Firestore.instance.collection('users').document(currentUser.uid);
Stream<DocumentSnapshot> docs = userDocRef.snapshots(includeMetadataChanges: false);
DocumentSnapshot data = await docs.firstWhere((DocumentSnapshot snapshot) => snapshot?.data !=null && snapshot.data.containsKey('createdAt'));
print('data ${data.toString()}');
IdTokenResult idTokenResult = await (currentUser.getIdToken(refresh: true));
print('claims : ${idTokenResult.claims}');
}
Hopefully this will help somebody else looking to do similar.
The code you're showing is likely trying to get custom claims too soon after the account is created. It will take a few seconds for the function to trigger after you call auth.createUserWithEmailAndPassword. It runs asynchronously, and doesn't at all hold up the process of user creation. So, you will need to somehow wait for the function to complete before calling user.getIdToken(refresh: true).
This is precisely the thing I address in this blog post. The solution I offer does the following:
Client: Creates a user
Client: Waits for a document with the user's UID to be created in Firestore
Server: Auth onCreate function triggers
Server: Function does its work
Server: At the end, function writes data to a new document with the new user's UID
Client: Database listener triggers on the creation of the document
Then, you would add more more step on the client to refresh the ID token after it sees the new document.
The code given in the post is for web/javascript, but the process applies to any client. You just need to get the client to wait for the function to complete, and Firestore is a convenient place to relay that information, since the client can listen to it in real time.
Also read this post for a way to get a client to refresh its token immediately, based on claims written to a Firestore document.
Bottom line is that you're in for a fair amount of code to sync between the client and server.