I'm learning GraphQL via https://github.com/the-road-to-graphql/fullstack-apollo-express-postgresql-boilerplate
and I'm wondering how to set cookies from a resolver as I'm used to using Express to do so.
signIn: async (
parent,
{ login, password },
{ models, secret },
) => {
const user = await models.User.findByLogin(login);
if (!user) {
throw new UserInputError(
'No user found with this login credentials.',
);
}
const isValid = await user.validatePassword(password);
if (!isValid) {
throw new AuthenticationError('Invalid password.');
}
return { token: createToken(user, secret, '5m') };
},
instead of returning a token obj, how can I access the response object and add a cookie?
You can achieve this using the context object, looking at the example you send. You will need to return the res variable from this function https://github.com/the-road-to-graphql/fullstack-apollo-express-postgresql-boilerplate/blob/master/src/index.js#L55
The context object is located at the 3rd argument to your resolver. The context is created on each request & available to all resolvers.
Example:
const server = new ApolloServer({
context: ({res}) => ({res})
});
function resolver(root, args, context){
context.res// express
}
Related
Im using Auth0 to authenticate users.
Im protected api routes like this:
// pages/api/secret.js
import { withApiAuthRequired, getSession } from '#auth0/nextjs-auth0';
export default withApiAuthRequired(function ProtectedRoute(req, res) {
const session = getSession(req, res);
const data = { test: 'test' };
res.json({ data });
});
My problem is when I'm trying to fetch the data from getServerSideProps I'm getting 401 error code.
If I use useEffect Im able to get data from api route.
Im trying to fetch the data like this:
export const getServerSideProps = withPageAuthRequired({
async getServerSideProps(ctx) {
const res = await fetch('http://localhost:3000/api/secret');
const data = await res.json();
return { props: { data } };
},
});
Im getting the following response:
error: "not_authenticated", description: "The user does not have an active session or is not authenticated"
Any idea guys? Thanks!!
When you call from getServerSideProps the protected API end-point you are not passing any user's context (such as Cookies) to the request, therefore, you are not authenticated.
When you call from useEffect it runs inside your browser, which attaches all cookies to the request, one of them is the session cookie.
You need to forward the session cookie that was passed to the getServerSideProps (by the browser) to the API call.
export const getServerSideProps = withPageAuthRequired({
async getServerSideProps(ctx) {
const res = await fetch('http://localhost:3000/api/secret', {
headers: { Cookie: ctx.req.headers.cookie },
// ---------------------------^ this req is the browser request to the getServersideProps
});
const data = await res.json();
return { props: { data } };
},
});
For more info.
#auth0/nextjs-auth0 has useUser hook. This example is from: https://auth0.com/blog/ultimate-guide-nextjs-authentication-auth0/
// pages/index.js
import { useUser } from '#auth0/nextjs-auth0';
export default () => {
const { user, error, isLoading } = useUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>{error.message}</div>;
if (user) {
return (
<div>
Welcome {user.name}! Logout
</div>
);
}
// if not user
return Login;
};
Note that authentication takes place on the server in this model,
meaning that the client isn't aware that the user is logged in. The
useUser hook makes it aware by accessing that information in the
initial state or through the /api/auth/profile endpoint, but it won't
expose any id_token or access_token to the client. That information
remains on the server side.
Custom HOF:
// getData is a callback function
export const withAuth = (getData) => async ({req, res}) => {
const session = await auth0.getSession(req);
if (!session || !session.user) {
res.writeHead(302, {
Location: '/api/v1/login'
});
res.end();
return {props: {}};
}
const data = getData ? await getData({req, res}, session.user) : {};
return {props: {user: session.user, ...data}}
}
Example of using:
export const getServerSideProps = withAuth(async ({req, res}, user) => {
const title = await getTitle();
return title;
});
I am using passport-jwt to verify access to a given route in express.js, and then return a Sequelize model to the final controller. The code looks like:
The auth strategy:
const passportStrategy = passport => {
const options = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.auth.ACCESS_TOKEN_SECRET
};
passport.use(
new Strategy(options, async (payload, done) => {
try {
const user = await User.findOne({ where: { email: payload.email }});
if (user) {
return done(null, {
user
});
}
return done(null, false);
}
catch (error) {
return done(error, false)
}
})
);
};
The route with the auth middleware
router.get('/:user_id/psy', passport.authenticate('jwt', { session: false }), patientsController.getPatientPsy);
The controller function
const getPatientPsy = async (req, res) => {
const authenticatedUser = req.user;
if (authenticatedUser.userType !== "patient") {
res.status(500).send("Big time error");
}
}
If I console.log(authenticatedUser) in the getPatientPsy() controller it successfully prints the Sequelize model with it's dataValues and so on, but when I try to access any property, be it userType or any other it consistently returns undefined.
In the passport-jwt authentication once a User has been found that matches the extracted JWT token, afaik it is returned synchronously and made it available in the req.user object, and I can print it with console.log, but why can't I access the model's properties?
I've tried to make the getPatientPsy() controller a sync function but it doesn't work either.
Thank you.
All right this is embarrassing, by default Passport.js returns the done(null, user) in the req.user property, and since I am returning { user }, I had to access through req.user.user.
My React Native app uses Amplify for a CUSTOM_AUTH authentication flow. The user receives a link via email to satisfy a challengeAnswer request. The process is like this:
User initiatiates sign in:
const cognitoUser = await Auth.signIn(username);
Email is sent to user via lambda.
User leaves app to retrieve email.
User clicks a link in the email which routes user back to the app via the RN Linking api.
The code from the link is processed with:
await Auth.sendCustomChallengeAnswer(
cognitoUser,
authChallengeAnswer
);
Usually this works well, but there is no guarantee that the cognitoUser object will exist after the app has been backgrounded while the user retrieves the email. There is a non-zero chance that iOS could dump the app during this time, and the cognitoUser var would be gone forcing the user to restart the sign in process. I'm looking for a way to persist the cognitoUser object somehow so if iOS decides the app needs to die this var can be retrieved from cache.
I'm able to cache the object into the Amplify cache (AsyncStorage) with
await Cache.setItem("cognitoUser", cognitoUser);
then fetch with
await Cache.getItem("cognitoUser");
which fails with
TypeError: user.sendCustomChallengeAnswer is not a function
because the process of caching it lost all its __proto__ functions. Its just retrieved as a basic object.
I suspect the cause is that I'm not using TypeScript, and the object loses some type information somehow.
Is there a better way of persisting this CognitoUser object so I can guarantee it exists after the user leaves/returns to the app as is needed in a CUSTOM_AUTH flow.
I use the following code to persist CognitoUser during sign in with CUSTOM_AUTH authentication flow:
import Auth from '#aws-amplify/auth'
import { CognitoUser } from 'amazon-cognito-identity-js'
const CUSTOM_AUTH_TTL = 5 * 60 * 1000 // Milliseconds
interface CustomAuthSession {
username: string
session: string
// Milliseconds after epoch
expiresAt: number
}
function clearCustomAuthSession() {
window.localStorage.removeItem('CustomAuthSession')
}
function loadCustomAuthSession(): CognitoUser {
const raw = window.localStorage.getItem('CustomAuthSession')
if (!raw) {
throw new Error('No custom auth session')
}
const storedSession: CustomAuthSession = window.JSON.parse(raw)
if (storedSession.expiresAt < window.Date.now()) {
clearCustomAuthSession()
throw new Error('Stored custom auth session has expired')
}
const username = storedSession.username
// Accessing private method of Auth here which is BAD, but it's still the
// safest way to restore the custom auth session from local storage, as there
// is no interface that lets us do it.
// (If we created a new user pool object here instead to pass to a
// CognitoUser constructor that would likely result in hard to catch bugs,
// as Auth can assume that all CognitoUsers passed to it come from its pool
// object.)
const user: CognitoUser = (Auth as any).createCognitoUser(username)
// Session is not exposed to TypeScript, but it's a public member in the
// JS code.
;(user as any).Session = storedSession.session
return user
}
function storeCustomAuthSession(cognitoUser: CognitoUser) {
// Session isn't exposed to TypeScript, but it's a public member in JS
const session = (cognitoUser as any).Session
const expiresAt = window.Date.now() + CUSTOM_AUTH_TTL
const otpSession: CustomAuthSession = {
session,
expiresAt,
username: cognitoUser.getUsername(),
}
const json = window.JSON.stringify(otpSession)
window.localStorage.setItem('CustomAuthSession', json)
}
You can reconstruct the CognitoUser object manually from your serialized object in localStorage or cache:
import { CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js';
const pool = new CognitoUserPool({
UserPoolId: cognitoObject.pool.userPoolId,
ClientId: cognitoObject.pool.clientId,
endpoint: cognitoObject.client.endpoint,
Storage: window.localStorage,
AdvancedSecurityDataCollectionFlag: cognitoObject.advancedSecurityDataCollectionFlag,
})
const cognitoUser = new CognitoUser({
Username: cognitoObject.username,
Pool: pool,
Storage: window.localStorage,
})
cognitoUser.Session = cognitoObject.Session
await Auth.completeNewPassword(cognitoUser, newPassword, cognitoObject.challengeParams)
Had to add the import statement myself, but got the general idea here: https://github.com/aws-amplify/amplify-js/issues/1715#issuecomment-800999983
I had this same issue and the simplest solution was to store it as a global variable within the slice.
authSlice.ts:
// we use this to temporarily store CognitoUser for MFA login.
// CognitoUser is not serializable so we cannot store it on Redux.
let cognitoUser = {};
export const doLogin = createAsyncThunk(
"auth/login",
async ({ email, password }: UserCredentials): Promise<Login | MFA> => {
const res = await Auth.signIn(email, password);
if (res.challengeName === "SOFTWARE_TOKEN_MFA") {
// we use this to temporarily store CognitoUser for MFA login.
// CognitoUser is not serializable so we cannot store it on Redux.
cognitoUser = res;
return {
status: "MFA",
user: null,
};
} else {
const user = await getUser();
return { user, status: "OK" };
}
}
);
export const confirmMFA = createAsyncThunk("auth/confirmMFA", async ({ mfa }: UserMFA) => {
if (!cognitoUser) {
throw new Error("Invalid flow?!");
}
await Auth.confirmSignIn(cognitoUser, mfa, "SOFTWARE_TOKEN_MFA");
const user = await getUser();
return { user, status: "OK" };
});
const getUser = async (): Promise<User> => {
const session = await Auth.currentSession();
// #ts-ignore https://github.com/aws-amplify/amplify-js/issues/4927
const { accessToken } = session;
if (!accessToken) {
throw new Error("Missing access token");
}
setCredentials(accessToken.jwtToken);
const user = await Auth.currentAuthenticatedUser();
return user.attributes;
};
Our requirement was also the same and we managed to get the customAuth flow working by creating cognitoUserPool and cognitoUser instance from localStorage/sessionStorage before calling sendCustomChallengeAnswer.
Example:
const userPoolData = {
Attributes: values(from localStorage);
}
const cognitoUserPool = new CognitoUserPool(userPoolData);
const userData = {
Attributes: values(from localStorage);
}
const cognitoUser = new CognitoUser(userData);
Auth.sendCustomChallengeAnswer(cognitoUser, validationCode);
I have been trying to implement react server-side-rendering using next, and redux-observable, now i want to implement auth
On signin
click signin
dispatch signin
set signin type
set signin data
call backend api auth/signin
if the response says that token is expired
call backed api auth/refresh using refreshToken
set cookie based on auth/refresh response token
set auth data based on auth/refresh response
else
set cookie based on auth/signin response token
set auth data based on auth/signin response
On accessing pages that needs auth
check for cookies called token
if exists
call backed api auth/me to authorize
if the response says that token is expired
call backed api auth/refresh using refreshToken
set cookie based on auth/refresh response token
set auth data based on auth/refresh
else
set auth data based on auth/me response
else
redirect to signin
Steps above happens inside the epics, as follows
/epics/signin.js
export const signinEpic = (action$, store) => action$
.ofType(SIGNIN)
.mergeMap(() => {
const params = { ... }
return ajax(params)
.concatMap((response) => {
const { name, refreshToken } = response.body
if (refreshToken && name === 'TokenExpiredError') {
const refreshParams = { ... }
return ajax(refreshParams)
.concatMap((refreshResponse) => {
setToken(refreshResponse.body.auth.token)
const me = { ... }
return [
authSetMe(me),
signinSuccess(),
]
})
.catch(error => of(signinFailure(error)))
}
const me = { ... }
setToken(response.body.auth.token)
return [
authSetMe(me),
signinSuccess(),
]
})
.catch(error => of(signinFailure(error)))
})
I did some console.log(Cookies.get('token')) to ensure that the cookie gets saved, and it prints the token just fine, saying that its there, but when i checked under browser console > Application > Cookies, nothing is there
So in auth epic below, the getToken() will always return '' which will always dispatch authMeFailure(error)
/epics/auth.js
// this epic will run on pages that requires auth by dispatching `authMe()`
export const authMeEpic = action$ => action$
.ofType(AUTH_ME)
.mergeMap(() => {
const params = {
...,
data: {
...
Authorization: getToken() ? getToken() : '', // this will always return ''
},
}
return ajax(params)
.mergeMap((response) => {
const { name, refreshToken } = response.body
if (refreshToken && name === 'TokenExpiredError') {
const refreshParams = { ... }
return ajax(refreshParams)
.mergeMap((refreshResponse) => {
setToken(refreshResponse.body.auth.token)
const me = { ... }
return authMeSuccess(me)
})
.catch(error => of(authMeFailure(error)))
}
const me = { ... }
setToken(response.body.auth.token)
return authMeSuccess(me)
})
.catch(error => of(authMeFailure(error)))
})
I use js-cookie for getting and setting cookies
EDIT: i actually prepared an auth lib containing getToken, setToken and removeToken, as follows
import Cookies from 'js-cookie'
export const isAuthenticated = () => {
const token = Cookies.get('token')
return !!token
}
export const getToken = () => Cookies.get('token')
export const setToken = token => Cookies.set('token', token)
export const removeToken = () => Cookies.remove('token')
and yes, i could have just used the setToken() on the epics, was just trying to directly test the cookie set method
UPDATE:
it seems that despite its not being in Console > Application > Cookies, its exists on every pages as it's printing the correct token if i do console.log(getToken()) inside the component render method
But every time i refresh the page, its gone. Kind of like it is being stored in a redux state, which is weird
UPDATE #2:
ok i think i manage to make it work, it turns out that we need 2 types of cookie, server side (the one's generated on refresh) and a client side (persist on navigating), so the reason that i wasn't able to get the token on epics its because it was not passed from the server side (at least this is my understanding)
Inspired by this issue comment on github
yarn add cookie-parser
on ./server.js (you need to have a custom server to be able to do this)
const cookieParser = require('cookie-parser')
...
server.use(cookieParser())
on ./pages/_document.js
export default class extends Document {
static async getInitialProps(...args) {
// ...args in your case would probably be req
const token = args[0].req ? getServerToken(args[0].req) : getToken()
return {
...
token,
}
}
render() {
...
}
}
on ./lib/auth.js or on any place you put your token methods
export const getServerToken = (req) => {
const { token = '' } = req.cookies
return token
}
export const getToken = () => {
return Cookies.get('token') ? Cookies.get('token') : ''
}
I am not 100% understand how this is solving my problem, but i am gonna leave it like this for now
I have just started with passport.js. From this article, I got what is the flow of all the passport methods and implemented the same in my application and it is working. Here is my server.js and I am using passport-local strategy. Angular app and rest APIs on the same server
import { registerControllersFromFolder } from 'giuseppe';
import { MessageManager } from './messaging/MessageManager';
import express = require('express');
import bodyParser = require('body-parser');
import session = require("express-session");
import http = require('http');
// class to hold user info
class User {
userId: number;
userName: string;
constructor(userId: number, userName: string) {
this.userId = userId;
this.userName = userName;
}
}
// server class to create http server
export class Server {
// list of apis for which authentication is not required
private static publicApiList: string[] = ["/services/login", "/login", "/favicon.ico"];
// request interceptor that will check user authentication
private static isAuthenticated = (req, res, next) => {
console.log("Authenticating :", req.originalUrl);
if (req.isAuthenticated() || Server.publicApiList.indexOf(req.originalUrl) > -1) {
// express routing
if (req.originalUrl.startsWith("/services")) {
console.log("Express Routing");
return next();
} else { // angular routing -> return index.html
console.log("Angular Routing");
return res.sendFile(__dirname + "/public/index.html");
}
} else {
console.log("User not authenticated.")
res.redirect('/');
}
};
static startServer() {
let userList: User[] = [new User(1, "Sunil"), new User(2, "Sukhi")];
let app = express();
// passport library
let passport = require('passport');
let LocalStrategy = require('passport-local').Strategy;
// middlewares
app.use(express.static(__dirname + "/public"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({ resave: false, saveUninitialized: true, secret: "secretKey123!!" }));
// passport middleware invoked on every request to ensure session contains passport.user object
app.use(passport.initialize());
// load seriliazed session user object to req.user
app.use(passport.session());
// Only during the authentication to specify what user information should be stored in the session.
passport.serializeUser(function (user, done) {
console.log("Serializer : ", user);
done(null, user);
});
// Invoked on every request by passport.session
passport.deserializeUser(function (user, done) {
let validUser = userList.filter(user => user.userId === user.userId)[0];
console.log("D-serializer : ", validUser);
done(null,validUser);
});
// passport strategy : Only invoked on the route which uses the passport.authenticate middleware.
passport.use(new LocalStrategy({
usernameField: 'name',
passwordField: 'password'
},
function (username, password, done) {
console.log("Strategy : Authenticating if user is valid :", username)
let user = userList.filter(user => username === user.userName);
console.log("Valid user : ", user)
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
return done(null, user[0]);
}
));
// intercept request for authentication
app.use(Server.isAuthenticated);
app.post('/services/login', passport.authenticate('local', {
successRedirect: '/profile',
failureRedirect: '/login'
}));
app.get('/services/logout', (req: any, res: any) => {
req.logout();
console.log("User Logout");
res.send("{status:'logout'}")
});
// http server creation
let server = http.createServer(app);
registerControllersFromFolder({ folderPath: './api' })
.then(router => {
app.use(router);
/* start express server */
})
.catch(err => {
/* error happened during loading and registering */
});
server.listen(7000, () => {
console.log('Up and running on port 7000');
});
}
}
exports.startServer = Server.startServer;
// Call a module's exported functions directly from the command line.
require('make-runnable');
When I hit localhost:7000 it serves the index.html page as I have used
app.use(express.static(__dirname + "/public"));
and this is an angular app and because of angular routing login module will get loaded by default. I have used a middleware that checks request authentication and if true then based on request prefix (angular or express) routing is done.
For the login request defined local strategy method is called and if this is true it calls serializer method that takes the responsibility which data should be stored in the request session. and then sucessRedirect or failureRedirect is called.
For subsequent request, As I have used middleware that checks if req.isAuthenticated is true if so then request is served otherwise the user is redirected to login page. I know in every subsequent request deserializeUser method is called that contains the object that was stored by serializeUser method in the login request. As per the document, this makes a call to the database to check valid user.
But I am confused but is the actual use case of deserializeUser method? Where can I take the benefit of this method and if I am intercepting ecah request and check req.isAuthenticted() then why to call database in deserializeUser method?>
As stated in this answer
The first argument of deserializeUser corresponds to the key of the
user object that was given to the done function (see 1.). So your
whole object is retrieved with help of that key. That key here is the
user id (key can be any key of the user object i.e. name,email etc).
In deserializeUser that key is matched with the in memory array /
database or any data resource.
The fetched object is attached to the request object as req.user
Thus, the benefit of deserializeUser is that you have the user object available on every request thereafter.
You ask why you need to use deserializeUser if you call req.isAuthenticated, and the answer lies in the implementation of req.isAuthenticated:
req.isAuthenticated = function() {
var property = 'user';
if (this._passport && this._passport.instance) {
property = this._passport.instance._userProperty || 'user';
}
return (this[property]) ? true : false;
};
To me, it looks like req.isAuthenticated is looking for req[user] to be set, and thus, deserializeUser must be called before it can work.