How to differentiate NextAuth user signin and signup? - next.js

I am building a blog platform using Nextjs 12, NextAuth(Google), Prisma(MySQL). When a user first signs up to my platform, NextAuth automatically saves user's google email address and google name to my database. I want to make user change their nickname when first signing up.
How would I know if the user is signing up or signing in? Currently in NextAuth, you click signup() button and you are good for both signup and signin..

I would extend the base user model with a property such as customName, and then check on the front- or back-end whether the customName property is undefined/empty, and redirect or show a modal accordinglty.
You can perform the front-end check via useSession
const { data: session, status } = useSession()
if(status === "authenticated" && session.user.customName === ""){ // or whatever your default value for non-defined fields is
// Show your modal or redirect to the page where the user can change his username
// after user enters his new name, make an API call and update it in your DB
}
or on the back-end in pages/api/auth/[...nextauth].js:
...
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
if (user.customName) {
return true
} else {
// User has no custom name yet, redirect him
return '/pathWhereUserCanSetHisName'
}
}
}
...
If you want to ensure every user has to set a name before continuing, I would put the logic above into an API middleware or next's edge middleware like this:
For example, if you're using NextAuth JWT sessions via cookies, add a custom cookie name for the sessionToken and parse it within the next.js middleware:
middleware.ts (or .js)
export default async function middleware(request: NextRequest) {
const response = NextResponse.next();
const userCookie = request.cookies.get(YOUR_CUSTOM_COOKIE_NAME) // the cookie name you set for NextAuth's sessionToken
if(!userCookie){
// user not logged in
return NextResponse.redirect('/login')
}
const user = yourCustomUserParsingFunction(userCookie) // parsing the JWT contained in the cookie
if(user.customName){
return response; // user has a custom name, don't intervene
}
// user has no customName, intervene
return NextResponse.redirect('/pathWhereUserCanSetHisName');
}

Related

Is there a way to generate a firebase email verification link before a user is actually signed up?

I am currently implementing a MFA system with Firebase Authentication & Google Authenticator.
Since my users are not allowed to authenticate with a non-verified email address, I'd like to prevent them from signing-in if their Firebase Authentication email_verified is set to false. To do that, I am using Google Cloud Identity Provider blocking functions, this works perfectly.
However, when it comes to the registration beforeCreate blocking function hook, I can't find a way to generate an email verification link for the user currently being created, the documentation says:
Requiring email verification on registration The following example
shows how to require a user to verify their email after registering:
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
const locale = context.locale;
if (user.email && !user.emailVerified) {
// Send custom email verification on sign-up.
return admin.auth()
.generateEmailVerificationLink(user.email)
.then((link) => {
return sendCustomVerificationEmail(
user.email, link, locale
);
});
}
});
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (user.email && !user.emailVerified) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
}
});
However, as far as I understand, generateEmailVerificationLink() can only be called to generate email verification link of an existing Firebase Authentication user. At this stage (while running beforeCreate blocking function), the user is not created yet.
Now I am wondering, I am missing something or is the Google documentation wrong?
No.
User data is created upon registration in the database.
Then, you may send an Email-Verification with a link automatically.
This Email-Verification just updates the field emaiVerified of said user data.
If you want to prevent users with unverified Emails from logging in, you need to adjust your Login page and check whether emaiVerified is true.
Important: Google will sign in a user right upon registration whether the email is verified or not, as this is the expected behavior from the perspective of a user. Email verification is ensured on the second, manual login.
(Also, please do not screenshot code.)
You can let a user sign in via email link at first, and call firebase.User.updatePassword() to set its password.
I am using Angular-Firebase, this is the logic code.
if (this.fireAuth.isSignInWithEmailLink(this.router.url)) {
const email = this.storage.get(SIGN_IN_EMAIL_KEY) as string;
this.storage.delete(SIGN_IN_EMAIL_KEY);
this.emailVerified = true;
this.accountCtrl.setValue(email);
from(this.fireAuth.signInWithEmailLink(email, this.router.url)).pipe(
catchError((error: FirebaseError) => {
const notification = this.notification;
notification.openError(notification.stripMessage(error.message));
this.emailVerified = false;
return of(null);
}),
filter((result) => !!result)
).subscribe((credential) => {
this.user = credential.user;
});
}
const notification = this.notification;
const info = form.value;
this.requesting = true;
form.control.disable();
(this.emailVerified ? from(this.user.updatePassword(info.password)) : from(this.fireAuth.signInWithEmailLink(info.account))).pipe(
catchError((error: FirebaseError) => {
switch (error.code) {
case AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY.POPUP_CLOSED_BY_USER:
break;
default:
console.log(error.code);
notification.openError(notification.stripMessage(error.message));
}
this.requesting = false;
form.control.enable();
return of(null);
}),
filter((result) => !!result)
).subscribe((result: firebase.auth.UserCredential) => {
if (this.emailVerified) {
if (result.user) {
notification.openError(`注册成功。`);
this.router.navigateByUrl(this.authService.redirectUrl || '');
} else {
notification.openError(`注册失败。`);
this.requesting = false;
form.control.enable();
}
} else {
this.storage.set(SIGN_IN_EMAIL_KEY, info.account);
}
});
Mate, if database won't create a new user using his email and password, and you send him email verification which will create his account, how the heck database will know his password? If it didn't create his account in the first step? Stop overthinking and just secure database using rules and routes in application if you don't want user to read some data while he didn't confirm email address.
It is that simple:
match /secretCollection/{docId} {
allow read, write: if isEmailVerified()
}
function isEmailVerified() {
return request.auth.token.email_verified
}
I think the blocking function documentation is wrong.
beforeCreate: "Triggers before a new user is saved to the Firebase Authentication database, and before a token is returned to your client app."
generateEmailVerificationLink: "To generate an email verification link, provide the existing user’s unverified email... The operation will resolve with the email action link. The email used must belong to an existing user."
Has anyone come up with a work around while still using blocking functions?
Using firebase rules to check for verification isn't helpful if the goal is to perform some action in the blocking function, such as setting custom claims.

Auth0 Passworldess sign up flow with Worpdress registration form

I m running a Wordpress site and I have set up Auth0 passwordless log in using the Lock Passwordless UI.
The passwordless authentication does not allow additional fields for the sign up and I successfully wrote a rule (not sure if is OK but it works) that redirects the user that logs in for the first time to a sign up form on Wordpress asking for more details from them.
function (user, context, callback) {
const namespace = 'https://mysiteurl/redirect';
const userType = context.stats && context.stats.loginsCount === 1 ? 'new' : 'existing';
context.idToken[`${namespace}userType`] = userType;
if (userType === 'new') {
context.redirect = {
url: "https://mys-site-url/sign-up-wordpress-form"
};
}
return callback(null, user, context);
}
What I would like to have is a permanent registration form on Wordpress where I collect data and the sign in process to be passwordless with email only.

NuxtJS state changes and firebase authentication

I am still a nuxt beginner, so please excuse any faults.
I am using the "official" firebase module for nuxt https://firebase.nuxtjs.org/ to access firebase services such as auth signIn and singOut.
This works.
However, I am using nuxt in universal mode and I cannot access this inside my page fetch function. So my solution is to save this info in the vuex store and update it as it changes.
So, once a user is logged in or the firebase auth state changes, a state change needs to happen in the vuex store.
Currently, when a user logs in or the firebase auth state changes, if the user is still logged in, I save the state to my store like so :
const actions = {
async onAuthStateChangedAction(state, { authUser, claims }) {
if (!authUser) {
// claims = null
// TODO: perform logout operations
} else {
// Do something with the authUser and the claims object...
const { uid, email } = authUser
const token = await authUser.getIdToken()
commit('SET_USER', { uid, email, token })
}
}
}
I also have a mutation where the state is set, a getter to get the state and the actual state object as well to store the initial state:
const mutations = {
SET_USER(state, user) {
state.user = user
}
}
const state = () => ({
user: null
})
const getters = {
getUser(state) {
return state.user
}
}
My problem is, on many of my pages, I use the fetch method to fetch data from an API and then I store this data in my vuex store.
This fetch method uses axios to make the api call, like so:
async fetch({ store }) {
const token = store.getters['getUser'] //This is null for a few seconds
const tempData = await axios
.post(
my_api_url,
{
my_post_body
},
{
headers: {
'Content-Type': 'application/json',
Authorization: token
}
}
)
.then((res) => {
return res.data
})
.catch((err) => {
return {
error: err
}
console.log('error', err)
})
store.commit('my_model/setData', tempData)
}
Axios needs my firebase user id token as part of the headers sent to the API for authorization.
When the fetch method runs, the state has not always changed or updated yet, and thus the state of the user is still null until the state has changed, which is usually about a second later, which is a problem for me since I need that token from the store to make my api call.
How can I wait for the store.user state to finish updating / not be null, before making my axios api call inside my fetch method ?
I have considered using cookies to store this information when a user logs in. Then, when inside the fetch method, I can use a cookie to get the token instead of having to wait for the state to change. The problem I have with this approach is that the cookie also needs to wait for a state change before it updates it's token, which means it will use an old token upon the initial page load. I might still opt for this solution, it just feels like it's the wrong way to approach this. Is there any better way to handle this type of conundrum ?
Also, when inside fetch, the first load will be made from the server, so I can grab the token from the cookie, however the next load will be from the client, so how do I retrieve the token then if the store value will still be null while loading ?
EDIT:
I have opted for SPA mode. After thinking long and hard about it, I don't really need the nuxt server and SPA mode has "server-like" behaviour, where you could still use asyncdata and fetch to fetch data before pages render, middleware still works similar and authentication actually works where you dont have to keep the client and server in sync with access tokens etc. I would still like to see a better solution for this in the future, but for now SPA mode works fine.
I came across this question looking for a solution to a similar problem. I had a similar solution in mind as mentioned in the other answer before coming to this question, what I was looking for was the implementation details.
I use nuxt.js, the first approach that came to my mind was make a layout component and render the <Nuxt/> directive only when the user is authenticated, but with that approach, I can have only one layout file, and if I do have more than one layout file I will have to implement the same pre-auth mechanism across every layout, although this is do-able as now I am not implementing it in every page but implementing in every layout which should be considerably less.
I found an even better solution, which was to use middlewares in nuxt, you can return a promise or use async-await with the middleware to stop the application mounting process until that promise is resolved. Here is the sample code:
// middleware/auth.js
export default async function ({ store, redirect, $axios, app }) {
if (!store.state.auth) { // if use is not authenticated
if (!localStorage.getItem("token")) // if token is not set then just redirect the user to login page
return redirect(app.localePath('/login'))
try {
const token = localStorage.getItem("token");
const res = await $axios.$get("/auth/validate", { // you can use your firebase auth mechanism code here
headers: {
'Authorization': `Bearer ${token}`
}
});
store.commit('login', { token, user: res.user }); // now just dispatch a login action to vuex store
}
catch (err) {
store.commit('logout'); // preauth failed, so dispatch logout action which will clear localStorage and our Store
return redirect(app.localePath('/login'))
}
}
}
Now you can use this middleware in your page/layout component, like so:
<template>
...
</template>
<script>
export default {
middleware: "auth",
...
}
</script>
One way of fixing this is to do the firebase login before mounting the app.
Get the token from firebase, save it in vuex and only after that mount the app.
This will ensure that by the time the pages load you have the firebase token saved in the store.
Add checks on the routes for the pages that you don't want to be accessible without login to look in the store for the token (firebase one or another) and redirect to another route if none is present.

Show multi-login popup when user is loggedin in multiple google account and bypass the login popup if user is loggedin in only one accout

I am working to add google sign-in flow on web app. Trying to figure out if we can achieve this functionality,
if user is logged-in in multiple google account, show the multi-login popup where user can choose the account.
if user is logged-in in only one google account and has previously signed into the application, bypass the login page and direct them to the application.
I thought auth2.signIn, it checks to see how many accounts are present, and based on this it achieve those above use-cases.
As further context, the javascript for the login page code is currently:
<script src="https://apis.google.com/js/platform.js?onload=init_google_signin" async defer></script>
<script>
function init_google_signin() {
var auhtInstance,
clientID = 'client_id';
auhtInstance = gapi.load('auth2', function () {
/**
* Retrieve the singleton for the GoogleAuth library and set up the
* client.
*/
auth2 = gapi.auth2.init({
client_id: clientID,
scope: 'email profile'
});
});
}
</script>
Further the google sign-in button click handler is written as,
function handleGoogleSignIn() {
var GoogleAuth,
currentUser,
googleAuthPromise;
GoogleAuth = gapi.auth2.getAuthInstance();
currentUser = GoogleAuth.currentUser.get();
if (currentUser && currentUser.isSignedIn()) {
// submit form with access_token and user info, service will redirect user to logged-in page
setGoogleFormValues(currentUser, currentUser.getBasicProfile());
} else {
googleAuthPromise = GoogleAuth.signIn();
googleAuthPromise.then(function (user) {
// submit form with access_token and user info, service will redirect user to logged-in page
setGoogleFormValues(currentUser, currentUser.getBasicProfile());
});
}
}
To Fix this, i tried,
when user logging-out from app, logout from google GoogleAuth.signOut();
when user clicks on google sign-in button, revoke access first then it will always prompt the login popup GoogleAuth.disconnect();
use prompt: 'select_account' while signin, so handleGoogleSignIn will look like,
function handleGoogleSignIn() {
var GoogleAuth,
currentUser,
googleAuthPromise;
GoogleAuth = gapi.auth2.getAuthInstance();
currentUser = GoogleAuth.currentUser.get();
googleAuthPromise = GoogleAuth.signIn(prompt: 'select_account');
googleAuthPromise.then(function (user) {
// submit form with access_token and user info, service will redirect user to logged-in page
setGoogleFormValues(currentUser, currentUser.getBasicProfile());
});
}
}
These all three fix always open the popup so it fixed the 1st use case but failed on 2nd use case.
I would really appreciate thoughts on how to achieve both use cases.
Many thanks!

User redirect and authentication with middleware of Nuxt

I'm trying to redirect a user to a login page if the user is not logged in when he tries to access certain pages with the following code.
// middlware/authenticated.js
import firebase from 'firebase'
export default function ({ store, redirect }) {
let user = firebase.auth().currentUser
store.state.user = user //this doesn't work
if (!user) {
console.log('redirect')
return redirect('/login')
}
}
However, the problem is with this code when I refresh a page I'm redirected to login page although without using the middleware, I can stay in the same page with logged in. For some reasons, which I don't know why, firebase can't work in middleware.
How should I modify this middleware or implement this function?
Thanks.
//middleware/authenticated.js
export default function ({
store,
redirect
}) {
if (!store.getters['index/isAuthenticated']) {
return redirect('/login')
}
}
//post.vue
async mounted () {
if (process.browser) {
let user;
if (!this.user) user = await auth(); // this auth is a plugin
await Promise.all([
this.user ? Promise.resolve() : this.$store.dispatch("setUser", { user: user || null })
]);
this.isLoaded = true;
}
},
//plugins/auth.js
import firebase from '~/plugins/firebase'
function auth () {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged((user) => {
resolve(user || false)
})
})
}
export default auth
By default Firebase persists the users logged in status on successful authentication. This example uses the session, to store the user uid and cookies to store the users token and used in situations where the sessions has ended (example when browser is closed) and then a new session started but where the user is still authenticated according to Firebase. In cases like these the user will not need to sign in to view protected resources.
Your basic Middleware to protect it should look like this (if you have a Store Module called User)
export default function ({ store, redirect }) {
if (!store.getters['modules/user/isAuthenticated']) {
return redirect('/auth/signin')
}
}
In your main Store you use the ServerInit Function to get the User if there is one saved in the Cookies and load it into your User Store Module which will be used for verification in the Middleware.
Your User Store Module should look like this, and keep in mind that you remove the Cookie when you Log the User out so that he is fully logged out.
I used the things i mentioned above as the beginning of my Authentication and modified it a bit, which you can also do. Most of the credit goes to davidroyer who has set up this nice Github Repo which includes all needed files as a good example on how to accomplish your goal.

Resources