Set CurrentUser (jwt) to #Post (in events controller) - postgresql

When a user makes an event
the logged in users id should be saved to the database
there is relation
#ManyToOne(_ => User, user => user.events, {
eager: true,
cascade: true
})
users: User;
in the events entity (many events possibly to one user)
my users entity relation
#OneToMany(_ => Event, event => event.users, {
eager: false
})
events: Event[];
in the database, all the fields (primary ID, name, description, image, startDate, endDate) show up plus a users_id.
That supposedly should take in the logged in users ID
the events controller has an
#Post decorator
#Authorized()
#Post("/events")
#HttpCode(201)
createEvent(#Body() event: Event) {
return event.save();
}
and in the front-end
my action sends all the values for the fields in the database
(when I create an event al the values are stored)
export const addEvent = event => (dispatch, getState) => {
const state = getState();
const jwt = state.currentUser.jwt;
if (isExpired(jwt)) return dispatch(logout());
request
.post(`${baseUrl}/events`)
.set("Authorization", `Bearer ${jwt}`)
.send({
name: event.name,
description: event.description,
startDate: event.startDate,
endDate: event.endDate,
image: event.image
})
.then(response =>
dispatch({
type: ADD_EVENT,
payload: response.body
})
);
};
I also send a jwt for the currentUser
and I have an example coming from
a boilerplate we got for learning to work with web-sockets
#Authorized()
#Post("/games/:id([0-9]+)/players")
#HttpCode(201)
async joinGame(#CurrentUser() user: User, #Param("id") gameId: number) {
const game = await Game.findOneById(gameId);
if (!game) throw new BadRequestError(`Game does not exist`);
if (game.status !== "pending")
throw new BadRequestError(`Game is already started`);
game.status = "started";
await game.save();
const player = await Player.create({
game,
user,
symbol: "o"
}).save();
io.emit("action", {
type: "UPDATE_GAME",
payload: await Game.findOneById(game.id)
});
return player;
}
there when a new game is created
it also stores the user that created the game
So I figured that it has something to do with the
#CurrentUser() user: User
But I have no Idea
how to implement in this projects #Post eventsController
If somebody can tell me how
and with a short explanation of how and why that works
I will keep googling.

I changed the #Post
to
#Authorized()
#Post('/events')
#HttpCode(201)
async createEvent(
#CurrentUser() user: User,
#Body() event: Event,
) {
if (user instanceof User) event.users = user
const entity = event.save()
return { entity }
}
}
and apparently it needed a
currentUserChecker function
currentUserChecker: async (action: Action) => {
const header: string = action.request.headers.authorization
if (header && header.startsWith('Bearer ')) {
const [, token] = header.split(' ')
if (token) {
const { id } = verify(token)
return User.findOne(id)
}
}
return undefined
}
and I had to change the jwt.ts
from
import * as jwt from 'jsonwebtoken'
const secret = process.env.JWT_SECRET || '9u8nnjksfdt98*(&*%T$#hsfjk'
const ttl = 3600 * 4 // our JWT tokens are valid for 4 hours
interface JwtPayload {
id: number
}
export const sign = (data: JwtPayload) =>
jwt.sign({ data }, secret, { expiresIn: ttl })
export const verify = (token: string): { data: JwtPayload } =>
jwt.verify(token, secret) as { data: JwtPayload }
to
import * as jwt from 'jsonwebtoken'
const secret = process.env.JWT_SECRET || '9u8nnjksfdt98*(&*%T$#hsfjk'
const ttl = 3600 * 4 // our JWT tokens are valid for 4 hours
interface JwtPayload {
id: number
}
export const sign = (data: JwtPayload) =>
jwt.sign({ data }, secret, { expiresIn: ttl })
export const verify = (token: string): JwtPayload =>
jwt.verify(token, secret) as JwtPayload

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.

Cypress & Microsoft Authentication don't work properly

I am trying to launch a Microsoft authentication via Cypress following the authentication structure proposed by Juunas11 available here (demo on youtube).
However, the request sent to the microsoft authority falls in timeout at the first execution. The following ones are launched without any problem.
Would there be a better way to manage this Microsoft authentication?
Below, the whole of my code written in a step definition but which will be inserted later in a Cypress command.
import { sign, decode, JwtPayload } from 'jsonwebtoken';
Given('le conseiller {string}', () => {
let cachedTokenExpiryTime = new Date().getTime();
let cachedTokenResponse: any = null;
if (cachedTokenExpiryTime <= new Date().getTime()) {
cachedTokenResponse = null;
}
const { tenantId, clientId, clientSecret, apiScopes, username, password } = Cypress.env('loginMicrosoft');
const authority = `https://login.microsoftonline.com/${tenantId}`;
const environment = 'login.windows.net';
const buildAccountEntity = (
homeAccountId: string,
realm: string,
localAccountId: string,
clientInfo: object,
username: string,
name: string
) => {
return {
authorityType: "MSSTS",
clientInfo: sign(clientInfo, clientSecret).split('.')[1],
homeAccountId,
environment,
realm,
localAccountId,
username,
name
};
};
const buildIdTokenEntity = (homeAccountId: string, idToken: string, realm: string) => {
return {
credentialType: "IdToken",
homeAccountId,
environment,
clientId,
secret: idToken,
realm,
};
};
const buildAccessTokenEntity = (
homeAccountId: string,
accessToken: string,
expiresIn: number,
extExpiresIn: number,
realm: string,
scopes: string[]
) => {
const now = Math.floor(Date.now() / 1000);
return {
homeAccountId,
credentialType: 'AccessToken',
secret: accessToken,
cachedAt: now.toString(),
expiresOn: (now + expiresIn).toString(),
extendedExpiresOn: (now + extExpiresIn).toString(),
environment,
clientId,
realm,
target: scopes.map((s) => s.toLowerCase()).join(' '),
tokenType: 'Bearer'
};
};
const injectTokens = (tokenResponse: any) => {
const idToken: JwtPayload = decode(tokenResponse.access_token) as JwtPayload;
const localAccountId = idToken.oid || idToken.sid;
const realm = idToken.tid;
const homeAccountId = `${localAccountId}.${realm}`;
const name = idToken.name;
const clientInfo = {
"uid": localAccountId,
"utid": realm
}
const accountKey = `${homeAccountId}-${environment}-${realm}`;
const accountEntity = buildAccountEntity(
homeAccountId,
realm,
localAccountId,
clientInfo,
username,
name
);
const idTokenKey = `${homeAccountId}-${environment}-idtoken-${clientId}-${realm}--`;
const idTokenEntity = buildIdTokenEntity(
homeAccountId,
tokenResponse.id_token,
realm
);
const accessTokenKey = `${homeAccountId}-${environment}-accesstoken-${clientId}-${realm}-${apiScopes.join('')}-`;
const accessTokenEntity = buildAccessTokenEntity(
homeAccountId,
tokenResponse.access_token,
tokenResponse.expires_in,
tokenResponse.ext_expires_in,
realm,
apiScopes
);
const msalAccountCle = `msal.${clientId}.active-account`;
const msalAccountEntitee = localAccountId;
cy.window().then((win) => {
win.localStorage.setItem(accountKey, JSON.stringify(accountEntity));
win.localStorage.setItem(idTokenKey, JSON.stringify(idTokenEntity));
win.localStorage.setItem(accessTokenKey, JSON.stringify(accessTokenEntity));
win.localStorage.setItem(msalAccountCle, msalAccountEntitee);
})
};
const login = (cachedTokenResponse: any) => {
let tokenResponse: any = null;
let chainable: Cypress.Chainable = cy.visit('');
if (!cachedTokenResponse) {
chainable = chainable.request({
url: `${authority}/oauth2/v2.0/token`,
method: 'POST',
body: {
grant_type: 'password',
client_id: clientId,
client_secret: clientSecret,
scope: 'openid profile user.read',
username: username,
password: password,
},
timeout: 120000,
form: true,
log: true,
retryOnStatusCodeFailure: true,
retryOnNetworkFailure: true
});
} else {
chainable = chainable.then(() => {
return {
body: cachedTokenResponse,
};
});
}
chainable
.then((response) => {
console.log('réponse de microsoft : ', response.allRequestResponses);
injectTokens(response.body);
cy.window().then((win) => {
expect(win.localStorage.length).to.be.gte(0);
});
tokenResponse = response.body;
cy.getCookies().log;
})
.visit('')
.then(() => {
return tokenResponse;
})
.waitUntil(() => cy.get('.header-sub'));
return chainable;
};
login(cachedTokenResponse)
.then((tokenResponse) => {
cachedTokenResponse = tokenResponse;
cachedTokenExpiryTime = new Date().getTime() + 50 * 60 * 1000;
});
});

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();
}

Got an error in Express, cant send request

controllers/userController.js
import User from '../models/userModel.js'
import asyncHandler from 'express-async-handler'
import generateToken from '../utils/generateToken.js'
// #desc Auth user & get token
// #route POST /api/users/login
// #access Public
const authUser = asyncHandler(async(req, res) => {
const { email, password } = req.body
const user = await User.findOne({ email })
if(user && (await user.matchPassword(password))) {
res.json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
token: generateToken(user._id)
})
} else {
res.status(401)
throw new Error('Invalid email or Password')
}
})
// #desc Get user Profile
// #route GET /api/users/login
// #access Private
const getUserProfile = asyncHandler(async(req, res) => {
// res.json(req.user)
const user = await User.findById(req.user._id)
console.log('user', user)
if (user) {
res.json(user)
} else {
res.status(404)
throw new Error('User not Found')
}
})
export { authUser, getUserProfile }
middleware/errorMiddleWare.js
const notFound = (req, res, next) => {
const error = new Error(`Not Found - ${req.originalUrl}`)
res.status(404)
next(error)
}
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode
res.status(statusCode)
res.json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack
})
}
export { notFound, errorHandler }
middleware/authMiddleware.js
import jwt from 'jsonwebtoken'
import asyncHandler from 'express-async-handler'
import User from '../models/userModel.js'
const protect = asyncHandler(async(req, res, next) => {
let token
if(req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
try {
token = req.headers.authorization.split(' ')[1]
const decoded = await jwt.verify(token, process.env.JWT_SECRET)
req.user = await User.findById(decoded.id).select('-password')
next()
} catch (error) {
res.status(401)
throw new Error('Not Authorized, token failed')
}
}
if(!token) {
res.status(401)
throw new Error('Not Authorized')
}
next()
})
export { protect }
routes/userRoutes.js
import express from 'express'
const router = express.Router()
import { authUser, getUserProfile } from '../controllers/userController.js'
import { protect } from '../middleware/authMiddleware.js'
router.post('/login', authUser)
router.route('/profile').get(protect, getUserProfile)
export default router
I got an error in userController.js, error from my errorMiddleware.
Scenario :
If I send a response from "if statement". (after User.findById)
But if I send response before "if statement", it work (is not Good). But why? and how can I solve this (to send a response after using User.findById) ?
I got an Error in server console when I used scenario 1 or 2.
version
node 14.12.0
express 4.17.1
Done, I forgot to delete next() in middleware/authMiddleware.js to protect getUserProfile.

Accounts.createUser without username, password and email

My application is built with React, which is completely separate from Meteor. I use Asteroid to interface to Meteor which serves as backend only. I have manually created the Facebook login button at front end and want to pass the data fetched from Facebook to Accounts.createUser. This method asks for two parameters which is not available because I have formatted it like so:
const data = {
services: {
facebook: fb
},
profile: {
first_name: fb.first_name,
last_name: fb.last_name,
}
}
I have created a method as below but I failed to log the user in with appropriate token or what ever indicator that Meteor needed:
getLoginByExternalService(options) {
if (Meteor.userId()) throw new Meteor.Error('400',`Please logout ${Meteor.userId()}`);
const email = options.services.facebook.email
const facebookId = options.services.facebook.id
const user = {services: {}}
user.services = options.services
const users = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
if (!users.length) {
const userId = Accounts.insertUserDoc(options, user)
if (Meteor.isServer)
this.setUserId(userId)
else
Meteor.setUserId(userId)
return userId
} else {
if (Meteor.isServer)
this.setUserId(users[0]._id)
if (Meteor.isClient)
Meteor.setUserId(userId)
return {users, userId: Meteor.userId()}
}
}
How to properly log the user in?
Okay I already got the answer. I don't have to format the data return from facebook response. So here the implementation at the backend
getLoginByExternalService(resp) {
if (Meteor.userId()) Meteor.logout(Meteor.userId()) //who knows?
const accessToken = resp.accessToken
const identity = getIdentity(accessToken)
const profilePicture = getProfilePicture(accessToken)
const serviceData = {
accessToken: accessToken,
expiresAt: (+new Date) + (1000 * resp.expiresIn)
}
const whitelisted = ['id', 'email', 'name', 'first_name', 'last_name', 'link', 'username', 'gender', 'locale', 'age_range']
const fields = _.pick(identity, whitelisted)
const options = {profile: {}}
const profileFields = _.pick(identity, getProfileFields())
//creating the token and adding to the user
const stampedToken = Accounts._generateStampedLoginToken()
//hashing is something added with Meteor 0.7.x,
//you don't need to do hashing in previous versions
const hashStampedToken = Accounts._hashStampedToken(stampedToken)
let ref = null
_.extend(serviceData, fields)
_.extend(options.profile, profileFields)
options.profile.avatar = profilePicture
try {
ref = Accounts.updateOrCreateUserFromExternalService("facebook", serviceData, options);
} catch (e) {
if (e.reason === "Email already exists.") {
const existingUser = Meteor.users.findOne({ 'emails.address': identity.email })
if ( existingUser ) {
if ( identity.verified ) {
Meteor.users.update({ _id: existingUser._id }, { $set: { 'services.facebook': serviceData }})
ref = { userId: existingUser._id }
console.log(`Merged facebook identity with existing local user ${existingUser._id}`);
} else {
throw Meteor.Error(403, "Refusing to merge unverified facebook identity with existing user")
}
}
} else {
throw Meteor.Error(e.error, e.reason)
}
}
Meteor.users.update(ref.userId, {$push: {'services.resume.loginTokens': hashStampedToken}})
return {id: ref.userId, token: stampedToken.token}
}
so somewhere at the front end
asteroid.call("getLoginByExternalService", data).then(response => response)