I am trying to add custom user claims after user sign up, to define user role, using setCustomUserClaims:
/api/users/addUser.js
export default async (req, res) => {
const { displayName, email, password, role } = req.body;
if (!displayName || !password || !email || !role) {
return res.status(400).send({ message: 'Missing fields' });
}
try {
const { uid } = await admin.auth().createUser({
displayName,
email,
password,
});
await admin.auth().setCustomUserClaims(uid, role);
res.status(201).send(uid);
} catch (error) {
handleError(res, error);
}
};
This code checks for any change in the authentication state and sets user to the currently logged in user:
/utils/use-auth.js
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
// Router.push('/login');
}
});
in my /pages/user/home,jsx:
import { useAuth } from '../../utils/use-auth';
function home() {
const { user } = useAuth();
return (
<div>
<pre>{JSON.stringify(user, null, 4)}</pre>
<AdminLayout componentProps={{ selected: '1' }} Component={HomeContent} />
</div>
);
}
The displayed object doesn't have any custom claims.
when I check the firebase console, I find that the user is actually added.
Try using
admin.auth().setCustomUserClaims(uid, claims).then(() => {
// Do your stuff here
});
And verify the claims like
admin.auth().verifyIdToken(idToken).then((claims) => {
// check claims
if (claims) {
// do your stuff here
}
});
for more info check https://firebase.google.com/docs/auth/admin/custom-claims#node.js
Related
I am trying to verify user emails with JWT. My current set up is that a JWT is sent to a user when they try to log in if they do not have a confirmed email.
When the email is sent it composes a URL with the token and then sends the request to the server to verify the email. It worked great in postman as I could easily add the email that I want to verify in the body. But I can't think of a way how to do it in the browser.
This is the code that should verify the email.
confirmEmail = async (req, res, next) => {
const { email } = req.body
const param = req.params.token
const user = await userModel.findOne({email})
if(!user)
{
throw new HttpException(401, 'User not found')
}
if(user.confirmed)
{
throw new HttpException(401, 'User already confirmed')
}
if(!user.confirmed)
{
const confirmJWT = jwt.verify(param, process.env.SECRET_JWT)
if(!confirmJWT)
{
throw new HttpException(200, 'Token invalid')
}
const result = await userModel.emailConfirmed(email)
}
res.send('Database updated.')
}
This is the code that generates the JWT and sends it in an email.
if(!user.confirmed)
{
const emailToken = jwt.sign(
{
email: user.email
},
process.env.SECRET_JWT,
{
expiresIn: '15m'
}
)
console.log(emailToken)
emailModel.verifyEmail(email, emailToken)
throw new HttpException(401, 'Email not confirmed')
}
I was wondering if there is any way I can use the just the token to find the email of the user or is that not possible with JWT?
export const verifyEmail = () => {
try
{
return API()
.post(`/api/confirm/:token`, {}, {
params: {
token: store.user.authToken
},
email: store.user.email
})
.then(({data: userData}) => {
console.log('worked')
})
}
catch(error)
{
console.log(error)
}
}
import { verifyEmail } from '../../services/authAPI'
import { useUserStore } from '../../stores/user'
const store = useUserStore()
export default {
data()
{
return {
email: store.user.email
}
},
methods: {
async handleSubmit()
{
try
{
const response = await verifyEmail(this.email)
}
catch(err)
{
console.log(err)
}
}
}
}
</script>
Basically you do not need to send the email in the body as already encoded the email into the JWT. Once you do const verifiedToken = jwt.sign(token, secret key) You can do verifiedToken.email to grab the email.
I have been trying to set user data into Sentry's scope globally, so every time there's an error or event, user info is passed to it.
My app is built in Next.js, so naturally I added the config as it is in Sentry's documentation for Next.js.
I haven't got the idea on where to add the Sentry.setUser({id: user.Id}) method in order for it to set the user globally.
So far I have added it to the Sentry's _error.js file, inside the getInitialProps method:
import NextErrorComponent from 'next/error';
import * as Sentry from '#sentry/nextjs';
import { getUser } from '../lib/session';
const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => {
if (!hasGetInitialPropsRun && err) {
Sentry.captureException(err);
}
return <NextErrorComponent statusCode={statusCode} />;
};
MyError.getInitialProps = async (context) => {
const errorInitialProps = await NextErrorComponent.getInitialProps(context);
const { req, res, err, asPath } = context;
errorInitialProps.hasGetInitialPropsRun = true;
const user = await getUser(req, res);
// Set user information
if (user) {
console.log('Setting user');
Sentry.setUser({ id: user.Id });
}
else {
console.log('Removing user');
Sentry.configureScope(scope => scope.setUser(null));
}
if (res?.statusCode === 404) {
return errorInitialProps;
}
if (err) {
Sentry.captureException(err);
await Sentry.flush(2000);
return errorInitialProps;
}
Sentry.captureException(
new Error(`_error.js getInitialProps missing data at path: ${asPath}`),
);
await Sentry.flush(2000);
return errorInitialProps;
};
export default MyError;
But when trying to log errors, the user info doesn't show in Sentry, only the default user ip:
I have also tried setting the user after successful login, and still nothing..
Help is appreciated!!
Not sure if this is the right way, but the above solutions didn't work for me. So I tried calling setUser inside _app.tsx.
import { useEffect } from "react";
import { setUser } from "#sentry/nextjs";
import { UserProvider, useUser } from "#auth0/nextjs-auth0";
import type { AppProps } from "next/app";
function SentryUserManager() {
const { user } = useUser();
useEffect(() => {
if (user) {
setUser({
email: user.email ?? undefined,
username: user.name ?? undefined,
});
} else {
setUser(null);
}
}, [user]);
return null;
}
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<UserProvider>
<Component {...pageProps} />
<SentryUserManager />
</UserProvider>
);
}
Still not sure why this worked for me and the other solutions didn't, but figured it was worth sharing.
I would suggest using the callback handler to set your Sentry user context.
import { handleAuth, handleLogin, handleCallback } from "#auth0/nextjs-auth0";
import * as Sentry from "#sentry/nextjs";
import { NextApiHandler } from "next";
const afterCallback = (_req, _res, session, _state) => {
Sentry.setUser({
id: session.user.sub,
email: session.user.email,
username: session.user.nickname,
name: session.user.name,
avatar: session.user.picture,
});
return session;
};
const handler: NextApiHandler = handleAuth({
async login(req, res) {
await handleLogin(req, res, {
returnTo: "/dashboard",
});
},
async callback(req, res) {
try {
await handleCallback(req, res, { afterCallback });
} catch (error) {
res.status(error.status || 500).end(error.message);
}
},
});
export default Sentry.withSentry(handler);
You can set the user in Sentry right after successful login
const handleLogin = {
try {
const res = await axios.post("/login", {"john#example.com", "password"})
if (res && res?.data) {
// Do other stuff
Sentry.setUser({ email: "john#example.com" });
}
}
}
Additionaly you can clear the user while logging out
const handleLogout = {
// Do othe stuff
Sentry.configureScope(scope => scope.setUser(null));
}
When the user clicks on the "sign-in" button and if user.challangeName === 'NEW_PASSWORD_REQUIRED'is true, I redirect the user to a page (form screen) where he can provide the input for required attributes and a new password. Even tho the user object is getting populated upon clicking on the sign-in button, using Auth.currentsession on the form screen will print "No user found"
Can someone let me know why I'm seeing no user? What am I doing wrong here?
Here's my login function (triggered when clicked on sign-in button) where I direct the user to the change password screen (form screen) if user.challangeName === 'NEW_PASSWORD_REQUIRED' is true.
const login = async (email, password) => {
try {
const user = await Auth.signIn(email, password);
if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
navigate('/change-password');
return;
}
if (user) {
setToken(user.signInUserSession.idToken.jwtToken);
const userDetails = await getAccountDetails();
dispatch({
type: LOGIN,
payload: {
user: {
attributes: user.attributes,
username: user.username
},
client: userDetails
}
});
}
} catch (error) {
await logout();
throw error;
}
};
Here's my onSubmit function on the change password screen where eventually I want to use Auth.completeNewPassword to update the user's password in Cognito
const onSubmitClick = (e) => {
e.preventDefault();
if (validateFields());
Auth.currentSession()
.then((user) => console.log(user))
.catch((err) => console.log(err));
};
Here's the documentation provided by AWS https://docs.amplify.aws/lib/auth/manageusers/q/platform/js/#forgot-password, and the code provided by AWS to update password
Auth.signIn(username, password)
.then(user => {
if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
const { requiredAttributes } = user.challengeParam; // the array of required attributes, e.g ['email', 'phone_number']
Auth.completeNewPassword(
user, // the Cognito User Object
newPassword, // the new password
// OPTIONAL, the required attributes
{
email: 'xxxx#example.com',
phone_number: '1234567890'
}
).then(user => {
// at this time the user is logged in if no MFA required
console.log(user);
}).catch(e => {
console.log(e);
});
} else {
// other situations
}
}).catch(e => {
console.log(e);
});
New updated answer to reflect your updated post, change the onSubmitClick to the following:
const onSubmitClick = (e) => {
e.preventDefault();
if (validateFields());
Auth.currentAuthenticatedUser()
.then(user => {
console.log(user))
})
.then((data) => console.log(data))
.catch((err) => console.log(err));
};
You are looking at the documentation which shows how to use the user after Auth.signIn() (which was my previous answer), compared to what you use: Auth.currentAuthenticatedUser(). The proper documentation example you have to look at is this one: https://www.docs.amplify.aws/lib/auth/manageusers/q/platform/js
I’m refactoring a Google Books app from a Restful API to GraphQL, and I am stuck on a mutation not behaving the way I expect.
When a user fills out the form found on Signup.js the Mutation ADD_USER should create a user within Mongoose, this user should have a JWT token assigned to them, and user should be logged in upon successful execution of the Mutation.
Actions observed:
• Mutation is being fired off from the front end. When I open developer tools in the browser I can see the Username, Email and Password being passed as variables.
• I have tried console logging the token, and keep getting an undefined return
• When I try to run the mutation in the GraphQL sandbox I get a null value returned.
• When I console log the args in resolvers.js no value appears on the console, which tells me the request is not reaching the resolver.
SignupForm.js (React FE Page)
import React, { useState } from "react";
import { Form, Button, Alert } from "react-bootstrap";
import { useMutation } from "#apollo/client";
import { ADD_USER } from "../utils/mutations";
import Auth from "../utils/auth";
const SignupForm = () => {
// set initial form state
const [userFormData, setUserFormData] = useState({
username: "",
email: "",
password: "",
});
// set state for form validation
const [validated] = useState(false);
// set state for alert
const [showAlert, setShowAlert] = useState(false);
const [addUser] = useMutation(ADD_USER);
const handleInputChange = (event) => {
const { name, value } = event.target;
setUserFormData({ ...userFormData, [name]: value });
};
const handleFormSubmit = async (event) => {
event.preventDefault();
// check if form has everything (as per react-bootstrap docs)
const form = event.currentTarget;
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
try {
///Add user is not returning data. payload is being passed as an object
const response = await addUser({
variables: { ...userFormData },
});
if (!response.ok) {
throw new Error("OH NO!SOMETHING WENT WRONG!");
}
const { token, user } = await response.json();
console.log(user);
Auth.login(token);
} catch (err) {
console.error(err);
setShowAlert(true);
}
setUserFormData({
username: "",
email: "",
password: "",
});
};
Mutation.js
export const ADD_USER = gql`
mutation addUser($username: String!, $email: String!, $password: String!) {
addUser(username: $username, email: $email, password: $password) {
token
user {
username
email
}
}
}
`;
typeDefs.js
const { gql } = require("apollo-server-express");
const typeDefs = gql`
input SavedBooks {
authors: [String]
description: String
bookId: String
image: String
link: String
title: String
}
type Books {
authors: [String]
description: String
bookId: ID
image: String
link: String
title: String
}
type User {
_id: ID
username: String
email: String
password: String
savedBooks: [Books]
}
type Auth {
token: ID!
user: User
}
type Query {
me: User
}
type Mutation {
##creates a user profile through the Auth type, that way we can pass a token upon creation
addUser(username: String!, email: String!, password: String!): Auth
login(email: String!, password: String!): Auth
saveBook(bookData: SavedBooks): User
deleteBook(bookId: ID!): User
}
`;
module.exports = typeDefs;
resolvers.js
const { User, Book } = require("../models");
const { AuthenticationError } = require("apollo-server-express");
const { signToken } = require("../utils/auth");
const resolvers = {
Query: {
me: async (parent, args, context) => {
if (context.user) {
return User.findOne({ _id: context.user._id }).populate("books");
}
throw new AuthenticationError("You need to log in");
},
},
};
Mutation: {
//try refactoring as a .then
addUser: async (parent, args) => {
//create user profile
await console.log("resolver test");
console.log(args);
const user = await User.create({ username, email, password });
//assign token to user
const token = signToken(user);
return { token, user };
};
login: async (parent, { email, password }) => {
const user = User.findOne({ email });
if (!user) {
throw new AuthenticationError("Invalid Login Credentials");
}
const correctPw = await profile.isCorrectPassword(password);
if (!correctPw) {
throw new AuthenticationError("Invalid Login Credentials");
}
const token = signToken(user);
return { token, user };
};
saveBook: async (parent, { bookData }, context) => {
if (context.user) {
return User.findOneAndUpdate(
{ _id: context.user._id },
{ $addToSet: { savedBooks: bookData } },
{ new: true }
);
}
throw new AuthenticationError("You need to log in");
};
deleteBook: async (parent, { bookId }, context) => {
if (context.user) {
return User.findOneAndUpdate(
{ _id: contex.user._id },
//remove selected books from the savedBooks Array
{ $pull: { savedBooks: context.bookId } },
{ new: true }
);
}
throw new AuthenticationError("You need to log in");
};
}
module.exports = resolvers;
auth.js
const jwt = require("jsonwebtoken");
// set token secret and expiration date
const secret = "mysecretsshhhhh";
const expiration = "2h";
module.exports = {
// function for our authenticated routes
authMiddleware: function ({ req }) {
// allows token to be sent via req.query or headers
let token = req.query.token || req.headers.authorization || req.body.token;
// ["Bearer", "<tokenvalue>"]
if (req.headers.authorization) {
token = token.split(" ").pop().trim();
}
if (!token) {
return req;
}
// verify token and get user data out of it
try {
const { data } = jwt.verify(token, secret, { maxAge: expiration });
req.user = data;
} catch {
console.log("Invalid token");
return res.status(400).json({ message: "invalid token!" });
}
// send to next endpoint
return req;
},
signToken: function ({ username, email, _id }) {
const payload = { username, email, _id };
return jwt.sign({ data: payload }, secret, { expiresIn: expiration });
},
};
Basically, I have combed from front to back end looking for where I introduced this bug, and am stuck. Any suggestions or feedback is greatly appreciated.
I was able to figure out the issue. First, a syntax error on resolver.js was preventing my mutations from being read.
Next, I made the following adjustment to handleFormSubmit on SignupForm.js
try {
///Add user is not returning data. payload is being passed as an object
const {data} = await addUser({
variables: { ...userFormData },
});
console.log(data)
console.log(userFormData)
**Auth.login(data.addUser.token);**
} catch (err) {
console.error(err);
setShowAlert(true);
}
That way my FE was properly accounting for what my Auth Middleware was passing back after successful user creation. Thanks for your help xadm, being able to talk this out got me thinking about where else to attack the bug.
I am unable to save username with AsyncStorage in my app.
I am using Switch component, if the value is true, username is saved and on logout the username should persist.
AsyncStorage returns undefined
// function for user to toggle if username should be saved
toggleRememberMe = value => {
this.setState({ rememberMe: value })
if (value === true) {
//user wants to be remembered.
this.rememberUser();
} else {
this.forgetUser();
}
}
// function to save username
rememberUser = async () => {
try {
await AsyncStorage.setItem('user_userID', this.state.signInEmail);
} catch (error) {
console.log(error)
}
};
// function to get username
getRememberedUser = async () => {
try {
const username = await AsyncStorage.getItem('user_userID');
if (username !== null) {
return username;
}
} catch (error) {
console.log(error)
}
};
// function to forget username or remove
forgetUser = async () => {
try {
await AsyncStorage.removeItem('user_userID');
} catch (error) {
console.log(error)
}
};
// componentDidMount
async componentDidMount() {
const username = await this.getRememberedUser();
console.log(username)
this.setState({
signInEmail: username || "",
rememberMe: username ? true : false });
}
//render
<Switch
trackColor={{true: '#16752A'}}
value={this.state.rememberMe}
onValueChange={(value) => this.toggleRememberMe(value)}
/>
<Text>Remember Me</Text>
How can I resolve this?