Create a new User when authenticating with Google - firebase

How can I create a document user in firebase when I first authenticate or multiple times ???
Example: I choose to authenticate by google, Twitter .... by account abc#gmail.com ... and I want to save to collection "user" with document = User UId (google account).
With the certificate authentication method I don't know how to get the Google User User UID until it's added to the Authentication List.
signInWithGoogleAsync = async () => {
try {
const { type, accessToken } = await Google.logInAsync({
iosClientId: '1058889369323-kh44jtru0ar24qu24ebv1bu0ebrgvokd.apps.googleusercontent.com',
scopes: ['profile', 'email'],
});
if (type === 'success') {
this.setState({ spinner: true });
const credential = firebase.auth.GoogleAuthProvider.credential(null, accessToken);
firebase.auth().signInWithCredential(credential).catch(error => {console.log(error);});
///I know how to do this, authenticate 1 user and put it in the Authentication list when authenticating successfully the first time
} else {
return { cancelled: true };
}
} catch (e) {
return { error: true };
}
}

Related

Google OAuth Refresh Tokens not returning Valid Access Tokens

I have a Firebase application that authenticates a user and returns an access token that I can then use to access the Google Calendar and Sheets API. I also save the refreshToken. Sample code for authenticated token:
firebase
.signInWithGoogle()
.then(async (socialAuthUser) => {
let accessToken = socialAuthUser.credential.accessToken // token to access Google Sheets API
let refreshToken = socialAuthUser.user.refreshToken
this.setState({accessToken, refreshToken})
})
After 1 hour, the accessToken expires. Firebase auth provides a refresh token on the user object after sign-in
I use that refresh token to re-authenticate and get a new access_token by posting to:
https://securetoken.googleapis.com/v1/token?key=firebaseAppAPIKey
That new access token does not work for Google APIs anymore, and it doesn't have the authorized scopes anymore. I also try sending it to
https://www.googleapis.com/oauth2/v1/tokeninfo?access_token="refreshToken"
It gives me the error "Invalid token". When I use the original token from firebase, it works just fine.
Anyone else encountering a similar issue? I haven't figured out a way to refresh the original access token with the correct access scopes without making the user sign-out and sign-in again.
Thanks!
I was finally able to solve it after many attempts.
Posted detailed solution on Medium: https://inaguirre.medium.com/reusing-access-tokens-in-firebase-with-react-and-node-3fde1d48cbd3
On the client, I used React with the Firebase library, and on the server I used Node.js with the packages google-apis and the firebase-admin skd package linked to the same Firebase project.
Steps:
(CLIENT) Send a request to the server to generate an authentication link
(SERVER) Generate Auth Link and send it back to the client using the getAuthLink() from googleapis. Sign in with Google and handle the redirect.
(SERVER) On the redirect route, use the code from Google on the query string to authenticate the user and get his user credentials. Use these credentials to check if the user is registered on Firebase.
(SERVER) If the user is registered, get the access and refresh tokens using the oauth2.getTokens(code), update refresh token on the user profile in the database. If the user is not registered, create a new user with firebase.createUser(), also create the user profile on the database with the refresh token.
(SERVER) Use firebase.createCustomToken(userId) to send an id_token back to client and authenticate.
(SERVER) Use a res.redirect({access_token, referesh_token, id_token}) to send credentials back to client.
(CLIENT) On the client, use the signInWithCustomToken(id_token) to authenticate, also restructure the query to obtain access_token and refresh_token to send API calls.
(CLIENT) Set an expiration date for the access token. On each request, check if the current date is higher than the expiration date. If it is, request a new token to https://www.googleapis.com/oauth2/v4/token with the refresh token. Otherwise use the access_token stored.
Most stuff happens when handling the Google Redirect after authentication. Here's an example of handling auth and tokens on the backend:
const router = require("express").Router();
const { google } = require("googleapis");
const { initializeApp, cert } = require("firebase-admin/app");
const { getAuth } = require("firebase-admin/auth");
const { getDatabase } = require("firebase-admin/database");
const serviceAccount = require("../google-credentials.json");
const fetch = require("node-fetch");
initializeApp({
credential: cert(serviceAccount),
databaseURL: "YOUR_DB_URL",
});
const db = getDatabase();
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
"http://localhost:8080/handleGoogleRedirect"
);
//post to google auth api to generate auth link
router.post("/authLink", (req, res) => {
try {
// generate a url that asks permissions for Blogger and Google Calendar scopes
const scopes = [
"profile",
"email",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/calendar",
];
const url = oauth2Client.generateAuthUrl({
access_type: "offline",
scope: scopes,
// force access
prompt: "consent",
});
res.json({ authLink: url });
} catch (error) {
res.json({ error: error.message });
}
});
router.get("/handleGoogleRedirect", async (req, res) => {
console.log("google.js 39 | handling redirect", req.query.code);
// handle user login
try {
const { tokens } = await oauth2Client.getToken(req.query.code);
oauth2Client.setCredentials(tokens);
// get google user profile info
const oauth2 = google.oauth2({
version: "v2",
auth: oauth2Client,
});
const googleUserInfo = await oauth2.userinfo.get();
console.log("google.js 72 | credentials", tokens);
const userRecord = await checkForUserRecord(googleUserInfo.data.email);
if (userRecord === "auth/user-not-found") {
const userRecord = await createNewUser(
googleUserInfo.data,
tokens.refresh_token
);
const customToken = await getAuth().createCustomToken(userRecord.uid);
res.redirect(
`http://localhost:3000/home?id_token=${customToken}&accessToken=${tokens.access_token}&userId=${userRecord.uid}`
);
} else {
const customToken = await getAuth().createCustomToken(userRecord.uid);
await addRefreshTokenToUserInDatabase(userRecord, tokens);
res.redirect(
`http://localhost:3000/home?id_token=${customToken}&accessToken=${tokens.access_token}&userId=${userRecord.uid}`
);
}
} catch (error) {
res.json({ error: error.message });
}
});
const checkForUserRecord = async (email) => {
try {
const userRecord = await getAuth().getUserByEmail(email);
console.log("google.js 35 | userRecord", userRecord.displayName);
return userRecord;
} catch (error) {
return error.code;
}
};
const createNewUser = async (googleUserInfo, refreshToken) => {
console.log(
"google.js 65 | creating new user",
googleUserInfo.email,
refreshToken
);
try {
const userRecord = await getAuth().createUser({
email: googleUserInfo.email,
displayName: googleUserInfo.name,
providerToLink: "google.com",
});
console.log("google.js 72 | user record created", userRecord.uid);
await db.ref(`users/${userRecord.uid}`).set({
email: googleUserInfo.email,
displayName: googleUserInfo.name,
provider: "google",
refresh_token: refreshToken,
});
return userRecord;
} catch (error) {
return error.code;
}
};
const addRefreshTokenToUserInDatabase = async (userRecord, tokens) => {
console.log(
"google.js 144 | adding refresh token to user in database",
userRecord.uid,
tokens
);
try {
const addRefreshTokenToUser = await db
.ref(`users/${userRecord.uid}`)
.update({
refresh_token: tokens.refresh_token,
});
console.log("google.js 55 | addRefreshTokenToUser", tokens);
return addRefreshTokenToUser;
} catch (error) {
console.log("google.js 158 | error", error);
return error.code;
}
};
router.post("/getNewAccessToken", async (req, res) => {
console.log("google.js 153 | refreshtoken", req.body.refresh_token);
// get new access token
try {
const request = await fetch("https://www.googleapis.com/oauth2/v4/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
refresh_token: req.body.refresh_token,
grant_type: "refresh_token",
}),
});
const data = await request.json();
console.log("google.js 160 | data", data);
res.json({
token: data.access_token,
});
} catch (error) {
console.log("google.js 155 | error", error);
res.json({ error: error.message });
}
});
module.exports = router;
For anyone who comes across this now, there is a much easier way at this point.
I was able to solve this by implementing a blocking function that simply saved the refreshToken and exiry date to firestore. You can then query this from your frontend to get the tokens there as well.
Be sure to enable the refreshToken in the firebase settings, otherwise the blocking function won't have access to it.
https://firebase.google.com/docs/auth/extend-with-blocking-functions
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import {
AuthEventContext,
AuthUserRecord,
} from "firebase-functions/lib/common/providers/identity";
admin.initializeApp();
exports.beforeSignIn = functions.auth
.user()
.beforeSignIn((user: AuthUserRecord, context: AuthEventContext) => {
// If the user is created by Yahoo, save the access token and refresh token
if (context.credential?.providerId === "yahoo.com") {
const db = admin.firestore();
const uid = user.uid;
const data = {
accessToken: context.credential.accessToken,
refreshToken: context.credential.refreshToken,
tokenExpirationTime: context.credential.expirationTime,
};
// set will add or overwrite the data
db.collection("users").doc(uid).set(data);
}
});

missing emails in firebase auth for 20% of facebook credentials

I allow users to login with facebook on my app, backed by firebase authentication.
In around 20% of the facebook logins, I don't receive the user's email. I need the email address in my app, and can't figure out why I don't receive it.
Since I get the email address 80% of the time, I assume I have the right permissions setup to retrieve it.
I also enforced "One account per email address" in firebase-auth, so it seems to be a different issue than that raised in Firebase Auth missing email address.
Relevant extracts of my code:
export const FacebookSignUp: React.FC<SocialAuthProps & { title?: string }> = ({ onError, onSetWaiting, title }) => {
async function onFacebookButtonPress() {
onSetWaiting(true);
const { email, first_name, accessToken } = await getFacebookUserData();
const couldLogin = await tryLoginWithFacebook(email, accessToken);
if (!couldLogin) {
// Create a Firebase credential with the AccessToken
const facebookCredential = FacebookAuthProvider.credential(accessToken);
const userCredential = await firebaseAuth.signInWithCredential(facebookCredential);
if (userCredential.user === null) {
throw new Error("Null user");
}
const signupUser: SignupUserData = {
userId: userCredential.user.uid,
email,
pseudo: first_name || undefined
};
await createSignupUser(signupUser).then(() => {
onSetWaiting(false);
});
}
}
return (
<SocialButton
iconName="facebookIcon"
title={title || "S'inscrire avec Facebook"}
onPress={() =>
onFacebookButtonPress().catch((err) => {
onSetWaiting(false);
if (err instanceof SocialAuthError) {
onError(err);
} else if (err instanceof Error) {
const { message, name, stack } = err;
serverError("Unexpected signup error", { message, name, stack });
}
})
}
/>
);
};
import { LoginManager, AccessToken, GraphRequest, GraphRequestManager } from "react-native-fbsdk";
export async function getFacebookUserData(): Promise<FacebookInfo> {
LoginManager.logOut();
const result = await LoginManager.logInWithPermissions(["public_profile", "email"]);
if (result.isCancelled) {
throw "User cancelled the login process";
}
// Once signed in, get the users AccesToken
const { accessToken } = (await AccessToken.getCurrentAccessToken()) || {};
if (!accessToken) {
throw "Something went wrong obtaining access token";
}
return new Promise((resolve, reject) => {
let req = new GraphRequest(
"/me",
{
httpMethod: "GET",
version: "v2.5",
parameters: {
fields: {
string: "email,first_name"
}
}
},
(err, res) => {
if (err || res === undefined) {
reject(err);
} else {
const { first_name, email } = res as { first_name: string; email: string };
resolve({ first_name, email, accessToken });
}
}
);
new GraphRequestManager().addRequest(req).start();
});
}
Facebook allows you to opt out of passing your email along to third-party apps. You can request it, but the user can deny it.
If I ever log in with Facebook I always opt out of passing my email along - most of the time, the third-party app doesn't need it for legitimate purposes.
"I need the email address in my app" - why? email marketing? account duplication prevention?
In cases where you did not get an email, assume the user has opted-out and/or doesn't have an email tied to their account. If you need one, ask the user to input a contact email address and explain what you are using it for. Expect some users to still opt out and plan around it.
You could always convert their username into a non-existent email like theirusername#noreply.users.yourapp.com depending on your use case.

OAuth2 fails to return auth token using simple-oauth2 and Firebase Functions for Spotify Authentication

I have been working on a oauth2 flow for spotify by following this similar tutorial by the Firebase team for Instagram HERE
I am able to submit my credentials and return the user code and state in the url, but when I run the method to submit the code to return an auth token, the auth token that I print to console in the Firebase functions returns: Auth Token Error Not Found. Here's my workflow:
Here's the Spotify docs
FIRST, I have a function to configure my spotifyOAuth:
function spotifyOAuth2Client() {
// Spotify OAuth 2 setup
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize'
},
};
return require('simple-oauth2').create(credentials);
}
I use that function in this Firebase function that is called using https://us-central1-<my project string>.cloudfunctions.net/redirect:
exports.redirect = functions.https.onRequest((req, res) => {
const oauth2 = spotifyOAuth2Client();
cookieParser()(req, res, () => {
const state = req.cookies.state || crypto.randomBytes(20).toString('hex');
console.log('Setting verification state:', state);
res.cookie('state', state.toString(), {
maxAge: 3600000,
secure: true,
httpOnly: true,
});
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
//scope: OAUTH_SCOPES,
state: state,
});
console.log('Redirecting to:', redirectUri);
res.redirect(redirectUri);
});
});
The code above returns a url string with the proper parameters, the following code block is where my code breaks, I have another cloud function that runs after being redirected from the res.redirect(redirectUri) above. And when I try to run the getToken() method, it appears to not return anything because I hit the catch block instead? This is where I observe the Auth Token Error Not Found.
const oauth2 = spotifyOAuth2Client();
try {
return cookieParser()(req, res, async () => {
console.log('Received verification state:', req.cookies.state);
console.log('Received state:', req.query.state);
if (!req.cookies.state) {
throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
} else if (req.cookies.state !== req.query.state) {
throw new Error('State validation failed');
}
console.log('Received auth code:', req.query.code);
console.log(OAUTH_REDIRECT_URI);
// Get the access token object (the authorization code is given from the previous step).
const tokenConfig = {
code: req.query.code,
redirect_uri: 'http://localhost:8100/popup'
};
// Save the access token
try {
const result = await oauth2.authorizationCode.getToken(tokenConfig)
const accessToken = oauth2.accessToken.create(result);
console.log('inside try');
console.log(result);
console.log(accessToken);
} catch (error) {
console.log('Access Token Error', error.message);
}
I've double checked my spotify client/secret credentials in the config, what is going wrong with this OAuth2 flow?
Resolved my issue, I was not using the correct endpoints:
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize',
tokenPath: '/api/token'
},
};

Nuxt SSR auth guard with Firebase auth

I'm trying to implement auth guards in Nuxt with Firebase Auth, but I keep running in to problems. At the moment I'm able to login, but the correct page isn't loaded after login, after login the user should be redirected to the '/profile-overview' page but that doesn't happen. When I navigate away from the 'profile' page to another page and then go back I do automatically go to the 'profile-overview' page. So the login works, there is just something wrong with the navigation / refresh of the page after login. Also when I refresh the page the user is logged out again, I would except the user to still be logged in then?
My code so far:
Page:
loginGoogle () {
this.$store.dispatch('signInWithGoogle').then(() => {
console.log('reload')
location.reload()
//window.location.reload(true)
}).catch((e) => {
this.title = 'Google login failed'
this.message =
"Something went wrong, please try again later. If this problem keeps happening please contact: jonas#co-house.be " + "Error: " + e.message;
this.dialog = true;
})
},
Middleware:
export default function ({ store, redirect, route }) {
console.log('user state' + store.state.user)
console.log('route ' + route.name)
store.state.user != null && route.name == 'profile' ? redirect('/profile-overview') : ''
store.state.user == null && isAdminRoute(route) ? redirect('/profile') : ''
}
function isAdminRoute(route) {
if (route.matched.some(record => record.path == '/profile-overview')) {
return true
}
}
Plugin:
import { auth } from '#/services/fireInit.js'
export default context => {
const { store } = context
return new Promise((resolve, reject) => {
auth.onAuthStateChanged(user => {
if (user) {
return resolve(store.commit('setUser', user))
}
return resolve()
})
})
}
Store (function to login only:
signInWithGoogle ({ commit }) {
return new Promise((resolve, reject) => {
auth.signInWithPopup(GoogleProvider).then((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;
return resolve(store.commit(state.user, result.user))
// ...
}).catch((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;
// ...
})
})
},
Does anyone have any idea what I could be doing wrong, or some documentation / tutorial I could read?
Thanks in advance.
You need to init your user on server in nuxtServerInit. See this repo for example implementation https://github.com/davidroyer/nuxt-ssr-firebase-auth.v2

WP API get user data

I'm authenticating users iva JWT Auth on WP Rest API from a React Native app, so, something like that:
export function userAuth(username, password) {
return dispatch => {
dispatch(requestLogin());
const appAuth = new FormData();
appAuth.append('username', username);
appAuth.append('password', password);
return fetch(wp.jwtEndpoint, {
method: 'POST',
body: appAuth
})
.then(function(res) {
return res.json();
})
.then(function(body) {
if(body.token){
getUserDataFromUsername(body.user_nicename, body.token, dispatch);
return dispatch(userSuccessLogin(body));
}
else {
return dispatch(userFailLogin(body));
}
});
}
}
The response for this request is:
{ token: 'eyJ0eXAiOiJKXXXQ',
user_email: 'supertest#gmail.com',
user_nicename: 'super-test-avatar',
user_display_name: 'TEST TEST' }
My issue is: as I can't get user ID from there, how can I retrieve user data form a request like https://www.wp.com/wp-json/wp/v2/users/${userId}?
I tried using https://github.com/dest81/wp-api-get-user-by-username, which would allow me to do that based on username, but its endpoints goes to 404, so I think it's outdated.

Resources