how to create gapi-script authentication using next-auth on nextjs project? - javascript

I am creating open-source project using google drive api, but I have a issue!
how to create gapi-script authentication using next-auth on nextjs project?
I can make authentication in gapi-script with out next-auth, also can make using next-auth using googleProvider.
But how to make authendication using gapi-script npm package with next-auth?
gapi-script auth
const initClient = () => {
setIsLoadingGoogleDriveApi(true);
gapi.client
.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES,
})
.then(
function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
},
function (error) {}
);
};
* Click to See the Full Code using gapi-cript

I solved a similar problem by creating a Gapi Context and passing the user's token into the context.
I had previously grabbed the users token during next auth and stored it in my DB
import React, { useState, createContext, useEffect, useCallback } from "react";
export const GapiClientContext = createContext();
const GAPI_CONFIG = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_CONFIG_apiKey,
clientId: process.env.GOOGLE_ID,
scope: "https://www.googleapis.com/auth/gmail.send",
discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"],
fetch_basic_profile: true,
immediate: true,
plugin_name: "app name",
};
const GapiContextProvider = ({ session, ...props }) => {
const [GapiClient, setGapiClient] = useState(); //{ gapi });
// to test initClient properly, remove access from third party access after each grant:
// https://myaccount.google.com/permissions?continue=https%3A%2F%2Fmyaccount.google.com%2Fsecurity%3Fpli%3D1
// then logout
// to ensure gapi only loads once: https://stackoverflow.com/questions/68985492/how-to-prevent-script-from-loading-multiple-times-with-react
const initClient = useCallback(async () => {
if (window.gapiIsInitialized) return;
console.log("intting gapi");
return gapi.client.init(GAPI_CONFIG).then(
() => {
const access_token =
session.externalAccounts.find((acct) => acct.provider === "gmail")
?.access_token ?? "";
if (access_token === "") return;
gapi.client.setToken({ access_token });
window.gapiIsInitialized = true;
setGmailIsDisabled(false);
return;
},
(e) => {
window.gapiIsLoading = false;
console.info("error init gapi client", e.details);
}
);
}, []);
const setupGapi = useCallback(async () => {
const gapi = await import("gapi-script").then((pack) => pack.gapi);
// https://stackoverflow.com/questions/71040050/why-am-i-getting-syntaxerror-cannot-use-import-statement-outside-a-module
setGapiClient({ gapi });
try {
await gapi.load("client:auth2", initClient);
} catch (e) {
window.gapiIsLoading = false;
console.log("couldnt sign in to gAPI!", e);
}
}, [initClient]);
useEffect(() => {
if (window.gapiIsInitialized || window.gapiIsLoading) return;
window.gapiIsLoading = true;
setupGapi();
}, [initClient, setupGapi]);
return (
<GapiClientContext.Provider
value={{
GapiClient
}}
>
{props.children}
</GapiClientContext.Provider>
);
};
export default GapiContextProvider;
Then in my application I wrapped the consuming components
A full example would show that I also tracked the state of sending email from within the context provider... you can see that here:
https://github.com/fotoflo/next-firebase-multi-auth-starter
... i'll be merging this context in in the next couple days also
<GapiContextProvider session={session}>
<SendGmailButton emailMessage={emailMessage} />
</GapiContextProvider>
Hope this is helpful! Good luck :-)

Related

Authenticated firebase functions with custom OpenId Connect provider

I have implemented a custom oidc authentication provider with firebase. (Which was very easy!)
For reference, the oidc provider I implemented is for Xero (accounting app)
I want to implement authenticated httpsCallable functions that use the accessToken that is returned from the callback but I can't seem to access it in the firebase function.
Ultimately the getTokenSetFromDatabase function from this example is what I need to recreate somehow in firebase function:
https://github.com/XeroAPI/xero-node#accounting-api
The context.auth info in the firebase functions contains some authentication data but not any jwts or tokens.
export const getTenants = functions.https.onCall(async (data, context) => {
await xero.initialize()
// Can I get the token set somehow from the context
// Or do I need to save a users token in the firebase database when they login from the front end?
const tokenSet = getTokenSetFromDatabase(context.auth?.uid)
await xero.setTokenSet(tokenSet)
if (tokenSet.expired()) {
const validTokenSet = await xero.refreshToken()
// save the new tokenset
}
await xero.updateTenants()
const activeTenantId = xero.tenants[0].tenantId
return activeTenantId
})
The console log of context.auth.token is:
{
"name": "Jialx",
"iss": "https://securetoken.google.com/firebase-app-name",
"aud": "firebase-app-name",
"auth_time": 1658994364,
"user_id": "0000000000000000000000",
"sub": "0000000000000000000000",
"iat": 1659007170,
"exp": 1659010770,
"email": "example#email.com",
"email_verified": false,
"firebase": {
"identities": { "oidc.xero": [], "email": [] },
"sign_in_provider": "oidc.xero",
"sign_in_attributes": {
"at_hash": "xx-xxxx-xxxx",
"preferred_username": "example#gmail.com",
"sid": "000000000000000000000000000",
"global_session_id": "000000000000000000000000000",
"xero_userid": "000000000000000000000000000"
}
},
"uid": "0000000000000000000000"
}
Discovery
So i've stumbled across the blocking functions feature when beforeSignIn function can access these oAuth credentials; so I figure that this would be a great place to save them to the DB and retrieve them later (what its built for).
However this doesn't seem to work with my custom OIDC auth provider config:
It does work but its buggy (See answer for details)
Okay so bit of strange behaviour in implementing the blocking function, It either takes a bit of time to apply that change, or its critically important to select the function AND THEN click the checkbox's AND THEN click save...
Either way, I've come back after lunch and now can see my refresh, id, and access tokens inside of the beforeSignIn context parameter.
Further down the Rabbit hole
I deployed a new version of my beforeSignIn function and found that my Additional provider token credentials options had been automatically toggled off...
SUMMARY
For completeness, this is how i've implemented OIDC Authenticated firebase functions that enable me to make authenticated calls and use my providers services (In this case a users accounting data).
Setup OpenID Connect following this guide:
Setup Firebase Functions following this guide
Build out your firebase functions with the requirements you have, This should give you a good start:
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
import { TokenSetParameters, XeroClient } from 'xero-node'
admin.initializeApp()
const scope = 'offline_access openid profile email accounting.transactions'
const xero = new XeroClient({
clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
clientSecret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
redirectUris: ['https://somefirebaseapp.firebaseapp.com/__/auth/handler'],
scopes: scope.split(' '),
httpTimeout: 3000,
})
const getTokenSetFromDatabase = (uid: string) =>
new Promise<TokenSetParameters>(async (res, rej) => {
await admin
.database()
.ref('user/' + uid + '/tokenSet')
.once('value', (snapshot) => {
const val = snapshot.val()
res(val)
})
})
export const getTenants = functions.https.onCall(async (data, context) => {
await xero.initialize()
const uid = context.auth?.uid
if (!uid) return null
const tokenSet = await getTokenSetFromDatabase(uid)
xero.setTokenSet(tokenSet)
try {
const newTokenSet = await xero.refreshToken()
await admin
.database()
.ref('user/' + uid + '/tokenSet')
.set(newTokenSet)
} catch (error) {
console.log(error)
}
await xero.updateTenants()
return xero.tenants
})
export const beforeSignIn = functions.auth.user().beforeSignIn(async (user, context) => {
if (!context.credential) return
const { accessToken, expirationTime, idToken, refreshToken } = context.credential
if (!accessToken || !expirationTime || !idToken || !refreshToken) return
const tokenSet: TokenSetParameters = {
access_token: accessToken,
expires_at: new Date(expirationTime).valueOf(),
id_token: idToken,
refresh_token: refreshToken,
scope,
}
await admin
.database()
.ref('user/' + user.uid + '/tokenSet')
.set(tokenSet)
})
Deploy your firebase functions and link your beforeSignIn function to the authentication > settings > blocking functions.
Keep in mind that if you deploy your function again you will need to reenable this. (Or it might be something that gets fixed 🤞)
You can actually enable this in the code...
export const beforeCreate = functions.auth
.user({ blockingOptions: { accessToken: true, idToken: true, refreshToken: true } })
.beforeCreate(async (user, conte....
Hopefully this helps the next person.. My codes not bulletproof of course, missing all the error handling and logging etc but you get what you pay for.

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '$store') in Nuxt js and firebase authentication

I am implementing firebase authentication to Nuxt js application and I am so close. The problem is I want to commit a vuext mutation inside firebase's default function onAuthStateChanged(). But when ever I load the page it shows the following error:
"Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '$store')"
Can you guys please help me out with this problem.
Thanks.
import firebase from '#/plugins/firebase'
import {
getAuth,
signInWithEmailAndPassword,
onAuthStateChanged
} from "firebase/auth"
export const state = () => ({
user: null,
authIsReady: false
})
export const mutations = {
updateUser(state, payload) {
state.user = payload
console.log('user is updated', state.user)
},
setAuthIsReady(state, payload) {
state.authIsReady = payload
console.log(state.authIsReady)
}
}
export const actions = {
async signIn(context, {
email,
password
}) {
console.log('sign in action')
const res = await signInWithEmailAndPassword(getAuth(), email, password)
if (res) {
context.commit('updateUser', res.user)
} else {
throw new Error('could not complete sign in')
}
}
}
// this function is causing the problem
const unsub = onAuthStateChanged(getAuth(), (user) => {
this.$store.commit('updateUser', user)
unsub()
})
The firebase.js file that I'm importing "auth" from below, is just all the regular setting up Firebase in Nuxt stuff... and the important lines are:
const auth = getAuth()
export { auth }
Try the code below ... I have mine in a file named "fireauth.js" in the plugins folder (don't forget to import the "fireauth.js" file in your nuxt.config.js)
import {
auth
} from "~/plugins/firebase.js";
export default (context) => {
const {
store
} = context
return new Promise((resolve, reject) => {
auth.onAuthStateChanged((user) => {
if (user) {
return resolve(store.dispatch('onAuthStateChangedAction', user))
}
return resolve()
})
})
}
In your store/index.js file add the following async function in your actions setting:
async onAuthStateChangedAction(vuexContext, authUser) {
if (!authUser) { //in my case I'm just forcing user back to sign in page, only authorized users allowed//redirect from here this.$router.push({
path: '/signin',
})
}else {
//call your commits or do whatever you want to do
vuexContext.commit("setUser", authUser.email);
}
},
The first part of the code ensures that when the auth state changes in Firestore, this change is communicated to the action that you just created in the store. The second part of the code, the async function in the store accomplishes whatever you want it to do within the store.

Vue created can not force to run non async

I have a Vue.js application and on the /callback route I am trying to have it do a few things. So far I am not having any luck with it because I am seeing things run async. I understand that it is normally how Vue/Javascript works however I am trying to force it to not be async.
The main issue I am having is sometimes the this.$store... are running before the items are set. This is an issue because of how things run on other Vuex actions. Mainly the getCompany one requires the loadToken one to complete as it is pulling the values from the local storage which is being set above.
I don't want to change this and how it works because of how the Vue router is set up to pull the token from local storage on each page reload. This token is used to connect to the backend so it needs to be pulled from local storage each reload as I don't want a user to log in just because they reload the page.
Code:
created() {
setTimeout(() => {
localStorage.setItem('token', this.$auth.token)
localStorage.setItem('user_data', JSON.stringify(this.$auth.user))
// Load company data
this.$store.dispatch('loadToken')
this.$store.dispatch('getCompany')
if(this.$auth == null || this.$auth.id_token['https://hello.io/account_signup_type/is_new']) {
this.$router.push('/setup')
} else {
// Load user data from Auth0
// Go to chat page
this.$router.push('/chat')
}
}, 500)
}
edit main.js code
import { Auth0Plugin } from '#/auth/auth0-plugin';
// Install the authentication plugin
Vue.use(Auth0Plugin, {
domain,
clientId,
audience,
onRedirectCallback: (appState) => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname,
);
},
});
auth0-plugin
/**
* External Modules
*/
import Vue from 'vue';
import createAuth0Client from '#auth0/auth0-spa-js';
/**
* Vue.js Instance Definition
*/
let instance;
export const getInstance = () => instance;
/**
* Vue.js Instance Initialization
*/
export const useAuth0 = ({
onRedirectCallback = () =>
window.history.replaceState({}, document.title, window.location.pathname),
redirectUri = `${window.location.origin}/callback`,
...pluginOptions
}) => {
if (instance) return instance;
instance = new Vue({
data() {
return {
auth0Client: null,
isLoading: true,
isAuthenticated: false,
user: {},
error: null,
token: null,
id_token: null
};
},
methods: {
async handleRedirectCallback() {
this.isLoading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
} catch (error) {
this.error = error;
} finally {
this.isLoading = false;
}
},
loginWithRedirect(options) {
return this.auth0Client.loginWithRedirect(options);
},
logout(options) {
return this.auth0Client.logout(options);
},
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
}
},
async created() {
this.auth0Client = await createAuth0Client({
...pluginOptions,
// responseType: 'id_token',
domain: pluginOptions.domain,
client_id: pluginOptions.clientId,
audience: pluginOptions.audience,
redirect_uri: redirectUri,
});
try {
if (
window.location.search.includes('code=') &&
window.location.search.includes('state=')
) {
const { appState } = await this.auth0Client.handleRedirectCallback();
onRedirectCallback(appState);
}
} catch (error) {
this.error = error;
} finally {
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.auth0Client.getUser();
this.$auth.getTokenSilently().then(token => this.token = token)
this.$auth.getIdTokenClaims().then(id_token => this.id_token = id_token)
this.isLoading = false;
}
},
});
return instance;
};
/**
* Vue.js Plugin Definition
*/
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
},
};
edit - updated router.beforeEach
router.beforeEach(async (to, from, next) => {
const auth = getInstance()
if(to.path == '/callback' && auth != null) {
console.log('Callback')
console.log(`Token: ${auth.token}`)
console.log(`User: ${JSON.stringify(auth.user)}`)
localStorage.setItem('token', auth.token)
localStorage.setItem('user_data', JSON.stringify(auth.user))
await store.dispatch('loadToken')
await store.dispatch('getCompany')
return next()
}
if(to.path != '/login' && to.path != '/setup') {
await store.dispatch('loadToken')
await store.dispatch('getCompany')
.then(() => {
return next()
})
} else {
return next()
}
})
edit - adding guide that I followed from Auth0 to get the code I have now - mostly
https://auth0.com/blog/complete-guide-to-vue-user-authentication/
The problem is that there is race condition because dispatch calls return promises that weren't chained before accessing the result of their work.
A good practice is to chain every promise, unless proven other wise.
The code that created contains actually belongs to the router in general because authentication logic is application-wide.
It's unnecessary to access global dependencies on this component instance. This is done for historical reasons because Vue originally was used in non-modular environment. In order to use outside components, global dependencies such as store need to be explicitly imported. In case this cannot be done, this needs to be fixed.
In this case auth instance is available through getInstance. In case the authentication shouldn't be done on each navigation, this needs to be done on condition, e.g.:
import { getInstance } from '.../auth';
import store from '.../store';
...
router.beforeEach(async (to, from, next) => {
const auth = getInstance();
if (...) {
...
await store.dispatch('loadToken')
await store.dispatch('getCompany')
...
next('/setup')
...
} else {
next()
}
})
getInstance doesn't serve a good purpose because it just exposes a variable. Instead, instance could be exported and imported directly, the behaviour would be the same due to how ES modules work.
Also global store already holds application logic and commonly used to handle authentication, including local storage operations.

How do I create custom tokens in Firebase using React?

I am trying to create a custom a custom token to log users in with their username. I've been through some of the documentation https://firebase.google.com/docs/auth/admin/create-custom-tokens#web, which was linked to me via How to provide user login with a username and NOT an email?, and I have seen that I need to add
Create custom tokens using the Firebase Admin SDK
and
Sign in using custom tokens on clients
At the moment I can kinda see what needs to be included based on the documentation, but I am unsure as to where this would go in the source code. Where do I add the code from the documentation? This is the source code for the userUser.js file, in case it helps.
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import firebase from "firebase/app";
import "firebase/auth";
import initFirebase from "../../config";
import {
removeUserCookie,
setUserCookie,
getUserFromCookie,
} from "./userCookie";
initFirebase();
export const mapUserData = async (user) => {
const { uid, email } = user;
const token = await user.getIdToken(true);
return {
id: uid,
email,
token,
};
};
const useUser = () => {
const [user, setUser] = useState();
const router = useRouter();
// this is most likely where the custom token for
// username goes
const logout = async () => {
return firebase
.auth()
.signOut()
.then(() => {
router.push("/");
})
.catch((e) => {
console.error(e);
});
};
useEffect(() => {
const cancelAuthListener = firebase
.auth()
.onIdTokenChanged(async (userToken) => {
if (userToken) {
const userData = await mapUserData(userToken);
setUserCookie(userData);
setUser(userData);
} else {
removeUserCookie();
setUser();
}
});
const userFromCookie = getUserFromCookie();
if (!userFromCookie) {
return;
}
setUser(userFromCookie);
return () => cancelAuthListener;
}, []);
return { user, logout };
};
export { useUser };
Any help would be greatly appreciated.
You can only use the admin sdk in a server environment (like in Firebase Functions or some other server) - you can't use it in the client environment where you're using React. Conceptually, the way this works is:
User enters a username and password in your client app
Client app sends the username and password to your server
Server checks the username and password and, if correct, creates a custom token using the admin SDK and sends that back to the client app
Client app uses that custom token to sign into Firebase
So it would look something like this (note - I don't handle any errors here but you'll want to):
// client.js
const sendToServer = (username, password) => {
// Step 1 - client sends the username/password to the cloud function
return axios.post(`${myCloudFunctionUrl}/login`, {
username,
password
}).then((response) => {
// Step 5 - the client logs the user in with the custom token
return firebase.auth().signInWithCustomToken(response.data.token)
}).then(() => {
// Step 6 - the user is now logged in and redirected to the dashboard
router.push("/dashboard")
})
}
// server.js (using Firebase Functions, but use whatever back end you want)
exports.login = functions.https.onRequest((req, res) => {
const {username, password} = req.body
// Step 2 - function verifies the username and password and gets the user's uid for the custom token
return verifyUserInDatabase(username, password).then((uid) => {
// Step 3 - the server creates a custom token
return admin.auth().createCustomToken(uid)
}).then((token) => {
// Step 4 - the server sends the token back in its response
res.json({ token })
})
})

Why is an extra await required in my vue.js unit test when more than one call is made to my firebase auth mock?

I have a vue.js app that uses firebase authentication. When unit-testing my Login.vue component, I mock firebase auth by mocking the config file I use to initialize firebase (named firebase.config). I should note that firebase.auth() is being exported from my config file as auth, hence the use of fb.auth and not fb.auth() in my code.
One of my tests checks that if the user is authenticated but not emailVerified, an email dialog is displayed. The dialog is not part of the test, but whether the dialogEmail data property is being set to true is what the test is asserting.
As part of the sign-in process, I set my firebase authentication state persistence to 'session'. Though this combination is not an issue for my app, it's giving me an issue in my test.
From Login.vue (abbreviated component code):
<template>
<v-btn #click="signInUser(email, password)">
Sign in
<v/btn>
</template>
<script>
import * as fb from '#/firebase.config';
export default {
data () {
return {
email: '',
password: '',
};
},
methods: {
async signInUser (email, password) {
try {
await fb.auth.setPersistence('session');
const { user } = await fb.auth.signInWithEmailAndPassword(email, password);
if (user.emailVerified) {
this.$router.push('/home');
} else {
this.dialogEmail = true;
}
} catch (err) {
// ...error handling
}
},
},
};
</script>
From login.spec.js:
import Vuetify from 'vuetify';
import { mount, createLocalVue } from '#vue/test-utils';
import Login from '#/views/Login';
jest.mock('#/firebase.config', () => ({
auth: {
setPersistence: () => Promise.resolve(),
signInWithEmailAndPassword: () => Promise.resolve({ user: { emailVerified: false } }),
},
}));
const localVue = createLocalVue();
const $router = { push: jest.fn() };
describe('login.vue', () => {
let vuetify;
beforeEach(() => {
vuetify = new Vuetify();
});
const mountFunction = () => mount(Login, {
localVue,
vuetify,
mocks: { $router },
});
it('shows dialog if user email not verified on sign in', async () => {
expect.assertions(1);
const wrapper = mountFunction();
const signInButton = wrapper.find('[data-test="signIn"]');
await wrapper.setData({ email: 'foo#bar.org', password: 'FooBarBaz' });
await signInButton.trigger('click');
expect(wrapper.vm.dialogEmail).toBe(true);
});
});
My app works fine. My test does not. It fails with an output of:
expect(received).toBe(expected) // Object.is equality
Expected: true
Received: false
However, my test WILL pass on one of two conditions:
I remove either of my await code snippets in my signInUser() method. This results in only one call to my mock function, which somehow helps.
I add a short wait to my test just before my assertion, like this (abbreviated test code):
await signInButton.trigger('click');
await new Promise(r => setTimeout(r, 0)); // my added code
expect(wrapper.vm.dialogEmail).toBe(true);
I'm elated that option #2 works, so that's what I'm doing. The question I have, however, is why does this extra timeout work, and why is it required? I'd like to understand what's happening, so I'm asking about it here.
In sign in user function, you are waiting for 2 promises to be resolved:
await fb.auth.setPersistence('session');
const { user } = await fb.auth.signInWithEmailAndPassword(email, password);
Inside test you added await signInButton.trigger('click'); but this will just trigger the function call, but not waiting for until its complete.
So you must add mock promise resolve inside test after you called the signin function.
setTimeout with 0 will add the promise at the end of the tasks queue so the test will be delayed there until the end of the function.

Categories