NextJS-Auth0: How can I assign a role on signup with Auth0? - reactjs

Using the library nextjs-auth0 (https://github.com/auth0/nextjs-auth0) I've been trying to make use of the handleAuth hook to grab a query arg to specify which role should be assigned to the user on signup.
An example of what I'm trying to do:
//pages/api/auth/[...auth0].js
const getLoginState = (req, loginOptions) => {
const { role } = req.query;
return { role: role };
};
export default handleAuth({
async login(req, res) {
try {
await handleLogin(req, res, { getLoginState } );
} catch (error) {
res.status(error.status || 500).end(error.message);
}
}
});
The documentation for handleAuth makes it seem like it's possible to do this ( https://auth0.github.io/nextjs-auth0/modules/handlers_login.html#getloginstate )
// from the documentation
const getLoginState = (req, loginOptions) => {
return { basket_id: getBasketId(req) };
};
From that doc - it looks like basket_id is the custom property to be saved against the user "before they visit the Identity Provider to login".
This sounds, to me, that basked_id will be saved somewhere against the users metadata once they've logged in. Is the documentation misleading, or am I misunderstanding?
How can I set the role during (or even slightly after) signup?

I managed to achieve what I wanted with the following Auth0 "rule":
function (user, context, callback) {
const count = context.stats && context.stats.loginsCount ? context.stats.loginsCount : 0;
if (count > 1) {
return callback(null, user, context);
}
const ManagementClient = require('auth0#2.27.0').ManagementClient;
const management = new ManagementClient({
token: auth0.accessToken,
domain: auth0.domain
});
let roles = [context.request.query.role];
const params = { id : user.user_id};
const data = { "roles" : roles};
management.users.assignRoles(params, data, function (err, user) {
if (err) {
// Handle error.
console.log(err);
}
callback(null, user, context);
});
}
Notice that the role is being read in from context.request.query.role. This pulls the query param role key off the login URL which more-or-less works how I wanted it to.
Then forward the role along from the auth in the backend:
const getLoginState = (req, loginOptions) => {
const { role } = req.query;
loginOptions.authorizationParams.role = role;
return { role: role };
};
export default handleAuth({
async login(req, res) {
try {
await handleLogin(req, res, { getLoginState });
} catch (error) {
res.status(error.status || 500).end(error.message);
}
}
});
Notice the loginOptions.authorizationParams.role = role;
So the login link can be set to: /api/auth/login?role=somerole and the rule will pick up the role and set it in the metadata part of the users info.
However: I wasn't able to get this to actually properly set the role on the user but it's enough for me, as it appears in the session.

Related

Why is my Azure redirect running twice and not stopping the fuction

I want to add the functionality for admins to disable end users access if necessary. It works just fine with non-SSO users. The check will prevent the user from logging in and show them a 'user is not active error'. When a non-active user tries to use Azure SSO to log in, the Azure SSO is still successful and displaying a spinner because there is not an active user. It should not allow them to 'log in' and redirect them to the home page with a displayed error that says 'user is not active'
Here is the function to change the user's isActive status on the backend
const changeUserStatus = asyncHandler(async (req, res) => {
const currentUser = await User.findById(req.user._id);
if (!currentUser) {
res.status(401);
throw new Error('User not found');
}
const user = await User.findByIdAndUpdate(req.params.id, req.body, {
new: true,
});
console.log(user);
res.status(201).json(user);
});
From the backend as well, here is the check for a user's isActive status in the normal login function
//check isActive status
if (user.isActive === false) {
res.status(400);
throw new Error('Not an active user');
}
Here is the check in the Azure SSO log in
if (!user.isActive) {
errors.azure = 'User is no longer permitted to access this application';
res.status(400);
throw new Error(errors.azure);
// console.log(errors);
// return res.status(401).json(errors);
}
Here is my authService.js
// Login user
const login = async (userData) => {
const response = await axios.post(API_URL + 'login', userData);
if (response.data) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
};
const azureLogin = async () => {
const response = await axios.get(API_URL + 'az-login');
return response.data;
};
Here is my authSlice
// Login user
export const login = createAsyncThunk('auth/login', async (user, thunkAPI) => {
try {
return await authService.login(user);
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
});
// Login user using AAD - this action sends the user to the AAD login page
export const azureLogin = createAsyncThunk(
'users/azureLogin',
async (thunkAPI) => {
try {
return await authService.azureLogin();
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
}
);
// Login user using AAD - this action redirects the user from the AAD login page
// back to the app with a code
export const azureRedirect = createAsyncThunk(
'users/azureRedirect',
async (code, thunkAPI) => {
try {
return await authService.azureRedirect(code);
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
}
);
And here is the AzureRedirect.jsx component. This is the component that receives the flow from the Microsoft/AAD login page. It is the re-entry point of the application, so to speak.
useEffect(() => {
const code = {
code: new URLSearchParams(window.location.search).get('code'),
};
if (user) {
toast.success(`Logged in as ${user.firstName} ${user.lastName}`);
navigate('/');
} else if (code) {
// This CANNOT run more than once
const error = dispatch(azureRedirect(code));
console.log(error);
} else {
console.log('No code found in URL');
}
}, [dispatch, navigate, user]);
if (!user) {
displayedOutput = <Spinner />;
} else {
displayedOutput = (
<div>
An error has been encountered, please contact your administrator.
<br />
<Link to='/login'>Return to Login</Link>
</div>
);
}
return <div className='pt-4'>{displayedOutput}</div>;

Is there a way to use ctx.session.$create in api using Blitz.js

I am trying to use blitz.js login API in a Flutter project. So I have created a /api/auth/login.ts file with the following code
import { getAntiCSRFToken, getSession, SecurePassword } from "#blitzjs/auth"
import { authenticateUser } from "app/auth/mutations/login"
import { AuthenticationError } from "blitz"
import db from "db"
import { Role } from "types"
const handler = async (req, res) => {
const session = await getSession(req, res)
const { email, password } = req.body
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` })
} else {
console.log("Create a new session for the user")
// Create a new session for the user
//login
const user = await authenticateUser(email, password)
const user = await db.user.findFirst({ where: { email } })
if (!user) return res.json({ data: "Hello", email, password })
const result = await SecurePassword.verify(user.hashedPassword, password)
const { hashedPassword, ...rest } = user
await req.session.$create({ userId: user.id, role: user.role as Role })
res.json({ rest })
}
export default handler
I also tried to use their docs but it was not clear enough and understandable
Can I use ctx.session.$create and insert it to db using blitz.js api
I have solved the problem using this code
import { Role } from "types"
import { authenticateUser } from "app/auth/mutations/login"
import { getSession } from "#blitzjs/auth"
export default async function customRoute(req, res) {
const session = await getSession(req, res)
const { email, password } = req.body
console.log(email, password)
console.log(session.$isAuthorized())
const user = await authenticateUser(email, password)
if (user.id === session.userId) {
return res.status(409).json({ error: `Already exist` })
}
await session.$create({ userId: user.id, role: user.role as Role })
// // res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify({ userId: session.userId }))
}
At first, I was getting a CSRF mismatch error and then a localStorage is undefined and now somehow everything is working with this code.

Firebase denies permission after first time then works on second time

Firebase denies permission after the first time to push data to realtime database... but then works when I push data on the second time...
export const writeClass = async (
courseId,
topicName,
classIntro,
youtubeLinkId,
classNoteLink
) => {
const dbRef = ref(getDatabase());
try {
const totalClasses = await get(
child(dbRef, `Courses/${courseId}/totalClasses`)
);
var totalClassesNum = parseInt(totalClasses.val()) + 1;
console.log(totalClasses.val());
const msg = await update(
ref(db, `Courses/${courseId}/classes/${totalClassesNum}`),
{
classIntro: classIntro,
topicName: topicName,
youtubeLinkId: youtubeLinkId,
classNoteLink: classNoteLink,
}
)
.then(() => {
update(ref(db, `Courses/${courseId}`), {
totalClasses: totalClassesNum,
});
console.log("Section added");
return true;
})
.catch((error) => {
console.log(error.message);
// alert(error.message)
return error.message;
});
return msg;
} catch (error) {
// alert(error.message)
console.log(error);
}
};
The firebase rules :
".write" :
"root.child('specialUsers').child(auth.uid).child('is_admin').val()
=== true
Firebase automatically restores the signed-in user when the page reloads/app restarts, but this requires it to call the server (to check a.o. whether the account was disabled) and that may take some time.
This means that the first time your code runs the user may not be signed in yet, and you need to check for that in your code. It's a bit unclear exactly where in your code the problem occurs, but you'll want to wrap that in:
if (firebase.auth().currentUser !== null) {
...
}
Even better would be to react to when the user is signed in or out, which you can do as shown in the first snippet in the documentation on getting the currently signed in user:
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});

How to pass additional data to a function that adds things to an object?

I am trying to create a user profile document for regular users and for merchants on Firebase. I am trying to add additional to data this document when a merchant signs up, but haven't succeeded. The difference is that merchants are supposed to have a roles array with their roles. If this is not the right approach to deal with differentiating users, I'd also be happy to hear what's best practice.
My userService file
async createUserProfileDocument(user, additionalData) {
console.log('additionalData: ', additionalData) //always undefined
if (!user) return
const userRef = this.firestore.doc(`users/${user.uid}`)
const snapshot = await userRef.get()
if (!snapshot.exists) {
const { displayName, email } = user
try {
await userRef.set({
displayName,
email,
...additionalData,
})
} catch (error) {
console.error('error creating user: ', error)
}
}
return this.getUserDocument(user.uid)
}
async getUserDocument(uid) {
if (!uid) return null
try {
const userDocument = await this.firestore.collection('users').doc(uid).get()
return { uid, ...userDocument.data() }
} catch (error) {
console.error('error getting user document: ', error)
}
}
This is what happens when the user signs up as a merchant in the RegisterMerchant component:
onSubmit={(values, { setSubmitting }) => {
async function writeToFirebase() {
//I can't pass the 'roles' array as additionalData
userService.createUserProfileDocument(values.user, { roles: ['businessOnwer'] })
authService.createUserWithEmailAndPassword(values.user.email, values.user.password)
await merchantsPendingApprovalService.collection().add(values)
}
writeToFirebase()
I am afraid this might have something to do with onAuthStateChange, which could be running before the above and not passing any additionalData? This is in the Middleware, where I control all of the routes.
useEffect(() => {
authService.onAuthStateChanged(async function (userAuth) {
if (userAuth) {
//is the below running before the file above and not passing any additional data?
const user = await userService.createUserProfileDocument(userAuth) //this should return the already created document?
//** do logic here depending on whether user is businessOwner or not
setUserObject(user)
} else {
console.log('no one signed in')
}
})
}, [])
There is onCreate callback function which is invoked when user is authenticated.
Here's how you could implement it
const onSubmit = (values, { setSubmitting }) => {
const { user: {email, password} } = values;
const additionalData = { roles: ['businessOnwer'] };
auth.user().onCreate((user) => {
const { uid, displayName, email } = user;
this.firestore.doc(`users/${uid}`).set({
displayName,
email,
...additionalData
});
});
authService.createUserWithEmailAndPassword(email, password);
}

set current user in context using apollo server

i have this middleware and need to get current user to set it in context of apollo server
app.use(async (req, res, next)=>{
const token = req.headers['authorization'];
if(token !== "null"){
try {
const currentUser = await jwt.verify(token, process.env.SECRET)
req.currentUser = currentUser;
} catch (error) {
console.log(error);
}
}
next()
})
and need to set current user in context
const SERVER = new ApolloServer({
schema,
context:{
currentUser //need to set this current user
}
})
SERVER.applymiddleware({app})
the context api in Apollo server offers handling request as follows
const initGraphQLserver = () => {
const graphQLConfig = {
context: ({ req, res }) => ({
user: req.user,
}),
rootValue: {},
schema,
};
const apolloServer = new ApolloServer(graphQLConfig);
return apolloServer;
};
This will assume that you will have proper middleware that will parse the cookie or header. This is dependent on your authentication mechanism as you need to have set user for the request in some middleware before, if you would like to use JWT, you can use for example this middleware
const auth = (req, res, next) => {
if (typeof req.headers.authorization !== 'string') {
return next();
}
const header = req.headers.authorization;
const token = header.replace('Bearer ', '');
try {
const jwtData = jwt.verify(token, JWT_SECRET);
if (jwtData && jwtData.user) {
req.user = jwtData.user;
} else {
console.log('Token was not authorized');
}
} catch (err) {
console.log('Invalid token');
}
return next();
};
This middleware will inject user if the jwt token is correct, then in your server file you will need to have the following order of middlewares
const app = express();
app.use(auth);
initGraphQLserver().applyMiddleware({ app });
Now you should have user in your schema context. I hope that it is clear, the code is not completed, but should be easy to finalize everything.

Resources