how to use jwt with graphql nodejs - jwt

Hello am new to graphql i want to create a signup page that when ever a user signs up his id and role in the users table in mysql data base will be encrypted with jwt, but when i tried it, it didnt return any thing,it returned this with graphiQL
{
"data": {
"registerUser": null
}
}
import models from '../../../models/index.js';
import User from '../../types/user.js';
import UserInput from '../../inputs/user.js';
const jsonwebtoken = require('jsonwebtoken')
require('dotenv').config()
export default {
type: User,
args: {
user: {
type: UserInput
}
},
resolve (_,args) {
models.user.create({
name: args.user.name,
email: args.user.email,
password: args.user.password,
role:"Student"
}).then(function(newUser) {
return jsonwebtoken.sign(
{ id: newUser.id, role: newUser.role },
process.env.JWT_SECRET,
{ expiresIn: '1y' }
)
});
}
};
pls how can i solve this problem so that it will return the jwt token

I think you only missed a return statement before models.user.create:
resolve (_,args) {
return models.user.create({
name: args.user.name,
email: args.user.email,
password: args.user.password,
role:"Student"
}).then(function(newUser) {
return jsonwebtoken.sign(
{ id: newUser.id, role: newUser.role },
process.env.JWT_SECRET,
{ expiresIn: '1y' }
)
});
}
It seems like you're interested in authentication with JWT and GraphQL. I think it's better to use the context and set the JWT to a cookie. I wrote a short article about this if you're interested.

Related

Persist session id in passport-saml login login callback

I'm using passport-saml and express-session. I login with my original session id but when the idp response reach the login callback handler, I have another sessionId. Also, since my browser has the session cookie with the original session id, it cannot use the new session id in the login callback, so I cannot authenticate.
interface SamlProvider {
name: string;
config: SamlConfig;
}
const providers: SamlProvider[] = [
{
name: process.env.SAML_ENTITY_ID_1!,
config: {
path: "/login/callback",
entryPoint: process.env.SAML_SSO_ENDPOINT_1,
issuer: process.env.SAML_ENTITY_ID_1,
cert: process.env.SAML_CERT_1!,
...(process.env.NODE_ENV === "production" && { protocol: "https" }),
disableRequestedAuthnContext: true,
},
},
{
name: process.env.SAML_ENTITY_ID_2!,
config: {
path: "/login/callback",
entryPoint: process.env.SAML_SSO_ENDPOINT_2,
issuer: process.env.SAML_ENTITY_ID_2,
cert: process.env.SAML_CERT_2!,
...(process.env.NODE_ENV === "production" && { protocol: "https" }),
disableRequestedAuthnContext: true,
},
},
];
export const samlStrategy = (sessionStore: session.Store) =>
new MultiSamlStrategy(
{
passReqToCallback: true, // makes req available in callback
getSamlOptions: function (request, done) {
// Find the provider
const relayState = request.query.RelayState || request.body.RelayState;
const provider = providers.find((p) => p.name === relayState);
if (!provider) {
return done(Error("saml identity provider not found"));
}
return done(null, provider.config);
},
},
async function (
req: Request,
profile: Profile | null | undefined,
done: VerifiedCallback
) {
if (profile && profile.nameID) {
const { nameID, nameIDFormat } = profile;
const email = profile[
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
] as string;
const firstName = profile[
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
] as string;
const lastName = profile[
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
] as string;
// Check if user is in risk database
const user = await myUserService.getByEmail(email);
if (!user) return done(new UserNotFoundError());
// If user has existing session, delete that existing session
sessionStore.all!((err: any, obj: any) => {
const sessions = obj as Array<{
sid: string;
passport?: { user?: { email?: string } };
}>;
const existingSess = sessions.find(
(sess) =>
sess.passport &&
sess.passport.user &&
sess.passport.user.email &&
sess.passport.user.email === email
);
if (existingSess && existingSess.sid) {
sessionStore.destroy(existingSess.sid, (err: any) => {
console.error(err);
return done(Error("failed to delete existing user session"));
});
}
});
return done(null, { nameID, nameIDFormat, email, firstName, lastName });
}
return done(Error("invalid saml response"));
}
);
Here's my login and login callback
app.post("/login/callback", async function (req, res, next) {
passport.authenticate("saml", (err: any, user: ISessionUser) => {
if (err) {
// TODO: Handle specific errors
logger.info({ label: "SAML Authenticate Error:", error: err });
return next(err);
} else {
req.logIn(user, (err) => {
if (err) {
logger.info({ label: "Login Error:", data: err });
return next(err);
}
res.redirect("/");
});
}
})(req, res, next);
});
app.get(
"/auth/saml/login",
passport.authenticate("saml", { failureRedirect: "/", failureFlash: true }),
function (req, res) {
res.redirect("/");
}
);
I experienced a similar issue using Microsoft 365 for authentication. The answer was to pass a randomly-generated nonce to the authentication request - this gets passed back to your app in the callback request. With SAML I think it depends on the provider whether they support such a flow, but it is good practice. You can also use a cookie to maintain state in your app, instead of, or additional to, the session id.

Get User ID from session in next-auth client

I'm using next-auth with Prisma and Graphql, I followed these instructions to set up the adapter and my models accordingly:
https://next-auth.js.org/adapters/prisma
Authentication works but when I inspect session object from here :
const { data: session, status } = useSession()
I don't see ID
The reason I need the ID is to make further GraphQL queries. I'm using email value for now to fetch the User by email, but having ID available would be a better option.
Here's the quickest solution to your question:
src/pages/api/auth/[...nextAuth].js
export default NextAuth({
...
callbacks: {
session: async ({ session, token }) => {
if (session?.user) {
session.user.id = token.uid;
}
return session;
},
jwt: async ({ user, token }) => {
if (user) {
token.uid = user.id;
}
return token;
},
},
session: {
strategy: 'jwt',
},
...
});
This worked for me.
callbacks: {
async jwt({token, user, account, profile, isNewUser}) {
user && (token.user = user)
return token
},
async session({session, token, user}) {
session = {
...session,
user: {
id: user.id,
...session.user
}
}
return session
}
}
Here's the quickest solution that worked for me
import NextAuth from "next-auth"
import { MongoDBAdapter } from "#next-auth/mongodb-adapter"
import clientPromise from "../../../lib/mongodb"
export const authOptions = {
providers: [
<...yourproviders>
],
callbacks: {
session: async ({ session, token, user }) => {
if (session?.user) {
session.user.id = user.id;
}
return session;
},
},
adapter: MongoDBAdapter(clientPromise),
}
I just referred to the NextAuth docs (this page) and finally got it working the right way
callbacks: {
jwt({ token, account, user }) {
if (account) {
token.accessToken = account.access_token
token.id = user?.id
}
return token
}
session({ session, token }) {
// I skipped the line below coz it gave me a TypeError
// session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
}
If you use TypeScript, add this to a new file called next-auth.d.ts
import NextAuth from 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
id: string;
} & DefaultSession['user'];
}
}
I believe you can change the callbacks so it includes the user's ID in the session: https://next-auth.js.org/configuration/callbacks.
You will need to change the JWT callback so it also include the userId and the session callback so the id is also persisted to the browser session.

I want to implement a sessions management system using Next-Auth

I want to implement a manage sessions system, so the user can logout all sessions when he change password.
1- when the user login I will store his session into user sessions array:
2- I'll check if the current session is stored in database, if not I'll log him out.
3- I want to add a "logout all sessions" button that logout all except for current session.
but I don't know how to start, because all I have when user login is:
{
user: { uid: '61a53559b7a09ec93f45f6ad' },
expires: '2021-12-30T16:34:58.437Z',
accessToken: undefined
}
{
user: { uid: '61a53559b7a09ec93f45f6ad' },
iat: 1638290097,
exp: 1640882097,
jti: '2fc85541-eb9b-475e-8261-50c874f85b51'
}
my [next-auth].js :
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import mongoose from "mongoose";
import { compare } from "bcrypt";
import { User } from "../auth/signup"
export default NextAuth({
//configure json web token
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60,
updateAge: 24 * 60 * 60,
},
providers: [
CredentialsProvider({
async authorize(credentials){
//connect to database
const client = await mongoose.connect(process.env.DB_URI);
//search for user using email or username
const result = await User.findOne({$or: [{email: credentials.username}, {username: credentials.username}]});
//if not found
if(!result){
client.connection.close();
throw new Error("Incorrect username or password.");
}
const checkPassword = await compare(credentials.password, result.password);
//of password doesn't match
if(!checkPassword){
client.connection.close();
throw new Error("Incorrect username or password.")
}
client.connection.close();
if(!result.emailVerified.verified){
client.connection.close();
throw new Error("Please verify your email adress.")
}
return {
uid: result._id
};
}
}),
],
callbacks: {
async jwt({ token, user, account }){
if (account) {
token.accessToken = account.access_token
}
user && (token.user = user)
return token
},
async session({ session, token }){
session.user = token.user;
session.accessToken = token.accessToken;
return session;
}
}
});
I created a sessions array for each user, when the user signs in, I generate a random hash key and save it to this array (you can add custom properties such as IP, time.. etc), and then save this hash to cookies, then I added a request in getServerSideProps
export async function getServerSideProps(context){
const {data: isAuthenticated} = await axios.get(`${process.env.WEB_URI}/api/auth/verify/session`, {
headers: {
cookie: context.req.headers.cookie
}
});
/api/auth/verify/session.js
export default async function session(req, res){
if(req.method === "GET"){
const session = await getSession({req: req});
if(!session){
return res.send(false);
}
mongoose.connect(process.env.DB_URI, {useUnifiedTopology: true} , async function(error) {
if(!error){
const user = await User.findOne({uid: session.user.uid}, {_id: 0, sessions: 1});
if(!user){
return res.send(false);
}
const userSession = user.sessions;
if(userSession.length > 0){
const tokens = userSession.map((session => session.token));
if(tokens.includes(session.user.token)){
res.send(true);
}else{
res.send(false);
}
}else{
res.send(false);
}
}
});
}
}
finally
if(props.isAuthenticated === false){
signOut();
}

Why mockReturnValueOnce seems not to work?

I'm trying to use Jest along Meteor, but I'm getting the following problem while trying to implement tests using Collection.find().fetch().
I'm mocking the function find and fetch, as you can see below. However, the value return by fetch is always the first one.
Shouldn't the value be different every time, since I'm using mockReturnValueOnce?
import { describe, test } from "#jest/globals";
import { Users } from "../../../collections"; // a Mongo collection
describe("#Users test suite", () => {
const mock1 = {
userId: "111aaa",
email: "user1#test.com",
};
const mock2 = {
userId: "222bbb",
email: "user1#test.com",
};
const mock3 = {
userId: "333ccc",
email: "user1#test.com",
};
test("Fetch test", () => {
Users.find.mockImplementation(() => ({
fetch: jest
.fn()
.mockReturnValueOnce(mock1)
.mockReturnValueOnce(mock2)
.mockReturnValueOnce(mock3),
}));
// getting same result in every fetch call
console.log(Users.find().fetch()); // { userId: '111aaa', email: 'user1#test.com' }
console.log(Users.find().fetch()); // { userId: '111aaa', email: 'user1#test.com' }
console.log(Users.find().fetch()); // { userId: '111aaa', email: 'user1#test.com' }
});
});
Thanks in advance!

Redirect to requested page after login using vue-router

In my application some routes are just accessible for authenticated users.When a unauthenticated user clicks on a link, for which he has to be signed in, he will be redirected to the login component.
If the user logs in successfully, I would like to redirect him to the URL he requested before he had to log in. However, there also should be a default route, in case the user did not request another URL before he logged in.
How can I achieve this using vue-router?
My code without redirect after login
router.beforeEach(
(to, from, next) => {
if(to.matched.some(record => record.meta.forVisitors)) {
next()
} else if(to.matched.some(record => record.meta.forAuth)) {
if(!Vue.auth.isAuthenticated()) {
next({
path: '/login'
// Redirect to original path if specified
})
} else {
next()
}
} else {
next()
}
}
)
My login function in my login component
login() {
var data = {
client_id: 2,
client_secret: '**************',
grant_type: 'password',
username: this.email,
password: this.password
}
// send data
this.$http.post('oauth/token', data)
.then(response => {
// authenticate the user
this.$auth.setToken(response.body.access_token,
response.body.expires_in + Date.now())
// redirect to route after successful login
this.$router.push('/')
})
}
This can be achieved by adding the redirect path in the route as a query parameter.
Then when you login, you have to check if the redirect parameter is set:
if IS set redirect to the path found in param
if is NOT set you can fallback on root.
Put an action to your link for example:
onLinkClicked() {
if(!isAuthenticated) {
// If not authenticated, add a path where to redirect after login.
this.$router.push({ name: 'login', query: { redirect: '/path' } });
}
}
The login submit action:
submitForm() {
AuthService.login(this.credentials)
.then(() => this.$router.push(this.$route.query.redirect || '/'))
.catch(error => { /*handle errors*/ })
}
I know this is old but it's the first result in google and for those of you that just want it given to you this is what you add to your two files. In my case I am using firebase for auth.
Router
The key line here is const loginpath = window.location.pathname; where I get the relative path of their first visit and then the next line next({ name: 'Login', query: { from: loginpath } }); I pass as a query in the redirect.
router.beforeEach((to, from, next) => {
const currentUser = firebase.auth().currentUser;
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (requiresAuth && !currentUser) {
const loginpath = window.location.pathname;
next({ name: 'Login', query: { from: loginpath } });
} else if (!requiresAuth && currentUser) next('menu');
else next();
});
Login Page
No magic here you'll just notice my action upon the user being authenticated this.$router.replace(this.$route.query.from); it sends them to the query url we generated earlier.
signIn() {
firebase.auth().signInWithEmailAndPassword(this.email, this.password).then(
(user) => {
this.$router.replace(this.$route.query.from);
},
(err) => {
this.loginerr = err.message;
},
);
},
I am going to be fleshing out this logic in more detail but it works as is. I hope this helps those that come across this page.
Following on from Matt C's answer, this is probably the simplest solution but there were a few issues with that post, so I thought it best to write a complete solution.
The destination route can be stored in the browser's session storage and retrieved after authentication. The benefit of using session storage over using local storage in this case is that the data doesn't linger after a broswer session is ended.
In the router's beforeEach hook set the destination path in session storage so that it can be retrieved after authentication. This works also if you are redirected via a third party auth provider (Google, Facebook etc).
router.js
// If user is not authenticated, before redirecting to login in beforeEach
sessionStorage.setItem('redirectPath', to.path)
So a fuller example might look something like this. I'm using Firebase here but if you're not you can modify it for your purposes:
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(x => x.meta.requiresAuth);
const currentUser = firebase.auth().currentUser;
if (requiresAuth && !currentUser) {
sessionStorage.setItem('redirectPath', to.path);
next('/login');
} else if (requiresAuth && currentUser) {
next();
} else {
next();
}
});
login.vue
In your login method, after authetication you will have a line of code that will send the user to a different route. This line will now read the value from session storage. Afterwards we will delete the item from session storage so that it is not accidently used in future (if you the user went directly to the login page on next auth for instance).
this.$router.replace(sessionStorage.getItem('redirectPath') || '/defaultpath');
sessionStorage.removeItem('redirectPath');
A fuller example might look like this:
export default Vue.extend({
name: 'Login',
data() {
return {
loginForm: {
email: '',
password: ''
}
}
},
methods: {
login() {
auth.signInWithEmailAndPassword(this.loginForm.email, this.loginForm.password).then(user => {
//Go to '/defaultpath' if no redirectPath value is set
this.$router.replace(sessionStorage.getItem('redirectPath') || '/defaultpath');
//Cleanup redirectPath
sessionStorage.removeItem('redirectPath');
}).catch(err => {
console.log(err);
});
},
},
});
If route guard is setup as below
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!loggedIn) {
next({
path: "/login",
query: { redirect: to.fullPath }
});
} else {
next();
}
} else {
next();
}
});
The redirect query can be extracted and used upon successful login
let searchParams = new URLSearchParams(window.location.search);
if (searchParams.has("redirect")) {
this.$router.push({ path: `${searchParams.get("redirect")}` });
} else this.$router.push({ path: "/dashboard" });
Another quick and dirty option would be to use local storage like the following:
In your beforeEach, before you redirect to login place the following line of code to save the initial requested path to local storage:
router.js
// If user is not authenticated, before redirecting to login
localStorage.setItem('pathToLoadAfterLogin', to.path)
Then in your login component, upon succesful login, you can redirect to the localStorage variable that you previously created:
login.vue
// If user login is successful, route them to what they previously requested or some default route this.$router.push(localStorage.getItem('pathToLoadAfterLogin') || 'somedefaultroute');
Much easier with this library
and login function is
let redirect = this.$auth.redirect();
this.$auth
.login({
data: this.model,
rememberMe: true,
redirect: { name: redirect ? redirect.from.name : "homepage", query: redirect.from.query },
fetchUser: true
})
This will help you #Schwesi .
Router.beforeEach(
(to, from, next) => {
if (to.matched.some(record => record.meta.forVisitors)) {
if (Vue.auth.isAuthenticated()) {
next({
path: '/feed'
})
} else
next()
}
else if (to.matched.some(record => record.meta.forAuth)) {
if (!Vue.auth.isAuthenticated()) {
next({
path: '/login'
})
} else
next()
} else
next()
}
);
This worked for me.
this.axios.post('your api link', {
token: this.token,
})
.then(() => this.$router.push(this.$route.query.redirect || '/dashboard'))
In Vue2 if someone has a routing and guarded some groups of routes. I solved this way.
function webGuard(to, from, next) {
if (!store.getters["auth/authenticated"]) {
sessionStorage.setItem("redirect", to); // hear I save the to
next("/login");
} else {
next();
}
}
Vue.use(VueRouter);
export default new VueRouter({
mode: "history",
hash: false,
routes: [
{
path: "/",
component: Home,
children: [
{ path: "", redirect: "home" },
...
...
],
beforeEnter: webGuard
},]
when you login
this.signIn({ email: test#gmail.com, password: 123 })
.then((res) => {
var redirectPath = sessionStorage.getItem('redirect');
sessionStorage.removeItem('redirect');
this.$router.push(redirectPath?redirectPath:"/dashboard");
})