So I have an app that logs me in via Auth0 and saves a jwt token in a cookie.
I also have an Apollo Server 2 that retrieves the data. How do I secure the Apollo Server and only return data if the user is logged in and verified by the Auth0 server?
The code below comes right from https://www.apollographql.com, but what I don't understand is how to handle getUser(token) below to actually check for a valid JWT in the Authorization header, and if present, the user will be allowed to access protected resources?
// using apollo-server 2.x
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// get the user token from the headers
const token = req.headers.authorization || '';
// try to retrieve a user with the token
const user = getUser(token);
// add the user to the context
return { user };
},
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
getUser is the method that returns your user with the given token. you might need to write that method yourself or use OAuth's getUser method.
After getting the user object, you're returning it so now you have access to the user object in your resolvers. In your resolver method, the third parameter is your context object. you can access the user object there. If you want to protect that resolver to only be allowed by logged in users you can throw an error if user is null or undefined.
For example:
export const resolvers = {
Query: {
Me: (parent, args, { user }) => {
if (!user) return Error(`Not Logged In!`)
return user
}
}
}
Related
I am working with Next-auth and rtk query. I need that when a request, any, returns a 401 unauthorized error, the page redirects directly to the login. How is it done?
I added 'maxAge: 60' to the [...nextauth].js file and also refetchInterval={30} refetchOnWindowFocus={true} to the component tried to find a similar solution, but it doesn't work
since you're using rtk query, you can update your apiSlice baseQuery function, to check for auth errors and redirect on that, my suggestion is this:
create a base query where you check for the 401 and any other error you want:
// try to execute the req, if it fails logout, and redirect to login.
const baseQueryWithAuth: BaseQueryFn = async (args, api, extraOptions) => {
const result = await baseQuery(args, api, extraOptions);
if (result.error?.status === 403 || result.error?.status === 401) {
// non authorized, then redirect to login page.
// if we have jwt, here u should update the access token
localStorage.removeItem(TOKEN_KEY_IN_LOCAL_STORAGE);
Router.replace('/auth/login');
}
return result;
};
in the snippet above, when I'm referring to token deletion as logout because the token is already invalid in the DB, so I just need to delete it in the front, so no invalidate request is needed.
the mentioned baseQuery can be done like this:
const baseUrl = `${process.env.NEXT_PUBLIC_API_PROTOCOL}://${process.env.NEXT_PUBLIC_API_HOST}/api`;
const TOKEN_KEY_IN_LOCAL_STORAGE = 'SavedToken';
const baseQuery = fetchBaseQuery({
baseUrl,
// credentials: 'include',
prepareHeaders: (headers) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem(TOKEN_KEY_IN_LOCAL_STORAGE);
if (token) {
headers.set('Authorization', token);
} else {
Router.replace('/auth/login');
}
return headers;
},
});
and then now since you have a working base query with auth support, you can use that to create a main rtk query apiSlice for your project:
// create api
export const apiSlice = createApi({
baseQuery: baseQueryWithAuth,
tagTypes: ['tag1', 'tag2', 'tag3'],
endpoints: (_builder) => ({}),
});
I am developing a microservices-based application using Apollo Gateway. Each service is written in Node.js and uses Graphql to build a federated schema. I am also using Mongoose to interact with a MongoDB database shared between the services. The main goal of the development of this app is to learn and gain experience using tools and technologies new to me as Graphql, microservices, and Node.js.
I have a question regarding authentication. I decided to use JWT based authentication with additional database-stored sessions for each user. In this way, I can monitor active sessions for each user and revoke access by disabling the session associated with the token. All of this is managed by the Auth service which is responsible for authenticating, creating new users and login/logout functions.
The Auth service exposes one REST endpoint to verify the jwt token as follows.
...
app.post('verify', async (req, res, next) => {
const token = req.body.jwt;
if(!token) {
res.status(403).send({ error: 'No token provided.' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findOne({ _id: decoded.sub});
if (!user) {
res.status(401).send({ error: 'No user found.' });
}
const session = await Session.findOne({
'_id': {
$in: user.sessions
}
});
if(!session) {
res.status(401).send({ error: 'Session not found or expired.' });
}
if(!session.valid) {
res.status(401).send({ error: 'Session not valid.' });
}
res.send({
userId: user.id,
scopes: user.scopes
});
}
const server = new ApolloServer({
schema: buildFederatedSchema([
{
typeDefs,
resolvers
}
]),
context: ({ req }) => {
return {
// headers
userId: req.get['user-id'] || 0,
scopes: req.get['user-scopes'] ? req.get['user-scopes'].split(',') : [],
// Mongoose models
models: {
User,
Session
}
}
}
});
server.applyMiddleware({ app, cors: false });
...
My API gateway is based on Apollo Gateway to build the federated schema. Authentication is verified by the Auth service and shared with every other service via request headers set by the gateway.
...
// Set authenticated user id in request for other services
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest({ request, context }) {
// pass the user's id from the context to underlying services
// as a header called `user-id`
request.http.headers.set('user-id', context.userId);
request.http.headers.set('user-scopes', context.scopes.join(','));
}
}
const gateway = new ApolloGateway({
serviceList: [
{ name: 'auth', url: 'https://auth:4000' }
],
buildService: ({ name, url }) => {
return AuthenticatedDataSource({ url });
}
});
// Apollo server middleware - last applied
const server = new ApolloServer({
gateway,
// not supported
subscriptions: false,
context: async ({ req }) => {
try {
// Send auth query to Auth service REST api
const response = await axios.post('https://auth:4000/verify', {
jwt: req.cookies['plottwist_login']
});
// save auth data in context
return {
userId: response.data.userId,
scopes: response.data.scopes
}
} catch(e) {
// deal with error
}
}
});
server.applyMiddleware({ app, path, cors: false });
...
This way the flow is the following:
API gateway receives the Graphql query request from the client.
API gateway queries the Auth service to authenticate the user using the only REST endpoint offered by the auth service (copying the token cookie from the received request).
Auth service authenticates the user and sends back data.
Gateway receives response, creates additional request headers and proceeds by managing the original Graphql query.
This comes with the cost of an extra call that the gateway makes before managing each Graphql query coming from the client. I wonder if this is a viable option or there is some major flaws in my reasoning.
Dears,
I'm trying to find how managing authentification on client side using the hhtp-only cookie sent by the server.
What I don't understand is that since the HTTP only cookie can't be accessed by the front end, how the front end knows that the user is (still) authenticated ?
So far, the only solution if found is to send to the client a token when the authentication succeed. And keep this token in a second cookie created by the client.
But it seems to me that I'm doing the same job twice.
1- managing the HTTP only cookie on server side, especially the expiration date
2- managing also on client side the expiration date of the second cookie.
How can avoid this ? I'd like to manage the authentification on client side based on the HTTP only server cookie. If there is a server cookie, then go on, else redirect to login page.
I'm using node/express on server side and react on client one. The session is stored in redis, both sides are HTTPS using certificates.
Thks
You don't need to store another cookie.
I suppose you use token based authentication on your endpoint, eg. JWT. Then you think about this scenario:
User send username/password to server.
Check user credentials and if there are valid, create http-only cookie with the token
const user = await getUser({ where: { email } });
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
throw new UserInputError('Form Arguments invalid', {
invalidArgs: {
'password': 'Invalid password!',
},
});
}
const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
/
res.cookie('token', token, {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 * 365,
});
Write auth middlerware to put the userId onto the req for future requests to access
const jwt = require('jsonwebtoken');
const { AuthenticationError } = require('apollo-server');
module.exports = async function(req, res, next) {
const { token } = req.cookies;
if (token) {
try {
const { userId } = jwt.verify(token, process.env.APP_SECRET);
if (!userId) return next();
req.userId = userId;
} catch (e) {
console.log(e);
}
}
next();
};
Check on each request the userId. If there is no userId, user doesn't logged in
if (!req.userId) {
throw new AuthenticationError('Log in!');
}
If user's token is invalid/expired you will get AuthenticationError. Catch it and redirect to login page.
If your UI depends on user status, you can create easy-to-use component (i am using React) to check it.
User Component:
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
import PropTypes from 'prop-types';
const CURRENT_USER_QUERY = gql`
query CURRENT_USER_QUERY {
me {
userId
firstName
lastName
profilePictureUrl
}
}
`;
const User = props => (
<Query {...props} query={CURRENT_USER_QUERY} fetchPolicy={'cache-first'}>
{payload => props.children(payload)}
</Query>
);
User.propTypes = {
children: PropTypes.func.isRequired,
};
export default User;
If we get me object from server, you know, there is a logged in user, so you can render depends on user's status:
import { Link } from 'react-router-dom';
import React from 'react';
<User>
{({ loading, error, data: { me } }) => {
if (loading || error || !me) return (
<Button component={Link} to={'/login'}>Login</Button>
);
if(me) return (
<Button component={Link} to={'/dashboard'}>Go to dashboard</Button>
)
}}
</User>
I am pretty new to loopback and here is what I am doing:
I am using standard login route provided by the loopback to log in the users - extended base Users to my own model say orgadmin.
With prebuilt route /api/orgadmin/login, I can easily login.
Now, I have a flag in orgadmins say 'status' which can be either 'active' or 'inactive' based on which I have to defer user login.
I was thinking something with remote hooks like beforeRemote as below but it doesn't work:
//this file is in the boot directory
module.exports = function(orgadmin) {
orgadmin.beforeRemote('login', function(context, user, next) {
console.log(user)
// context.args.data.date = Date.now();
// context.args.data.publisherId = context.req.accessToken.userId;
next();
});
};
So what is the best way to accomplish this?
The user attribute will only be available if the request is coming with a valid access token. The attribute is unused for unauthenticated requests, which login is.
Here's a possible alternative:
module.exports = (OrgAdmin) => {
OrgAdmin.on('dataSourceAttached', () => {
const { login } = OrgAdmin;
OrgAdmin.login = async (credentials, include) => {
const accessToken = await login.call(OrgAdmin, credentials, include);
const orgAdmin = await OrgAdmin.findById(accessToken.userId);
if (orgAdmin.status !== 'active') {
OrgAdmin.logout(accessToken);
const err = new Error('Your account has not been activated');
err.code = 'NOT_ACTIVE_USER';
err.statusCode = 403;
throw err
}
return accessToken;
};
});
};
The above code overrides the login method and does the following:
Login the user, using loopback's built-in login
Take the response of login, which is an access token, and use it to get the user.
If the user is active, return the access token, satisfying the expected successful response of login.
If the user is not active, remove the access token that was created (which is what logout does), and throw an error.
So far I have only dealt with server-rendered apps, where after a user logs in via username/password or using an OAuth provider (Facebook etc.), the server just sets a session cookie while redirecting to the relevant page.
However now I'm attempting to build an app using a more 'modern' approach, with React on the frontend and a JSON API backend. Apparently the standard choice for this is to use a JSON web token for authentication, however I'm having trouble working out how I'm meant to provide the JWT to the client so it can be stored in session/local storage or wherever.
Example to illustrate better:
User clicks link (/auth/facebook) to log in via Facebook
User is redirected and shown Facebook login form and/or permission dialog (if necessary)
Facebook redirects user back to /auth/facebook/callback with an authorization code in tow, the server exchanges this for an access token and some information about the user
Server finds or creates the user in the DB using the info, then creates a JWT containing a relevant subset of the user data (e.g. ID)
???
At this point I just want the user to be redirected to the main page for the React app (let's say /app) with the JWT in tow, so the frontend can take over. But I can't think of an (elegant) way to do that without losing the JWT along the way, other than to put it in the query string for the redirect (/app?authtoken=...) - but that will display in the address bar until I remove it manually using replaceState() or whatever, and seems a little weird to me.
Really I'm just wondering how this is typically done, and I'm almost sure I'm missing something here. The server is Node (Koa with Passport), if that helps.
Edit: To be clear, I'm asking what the best way is to provide a token to the client (so it can be saved) after an OAuth redirect flow using Passport.
I recently ran across this same issue, and, not finding a solution here or elsewhere, wrote this blog post with my in-depth thoughts.
TL;DR: I came up with 3 possible approaches to send the JWT to the client after OAuth logins/redirects:
Save the JWT in a cookie, then extract it on the front-end or server in a future step (eg. extract it on the client with JS, or send a request to the server, server uses the cookie to get the JWT, returns the JWT).
Send the JWT back as part of the query string (which you suggest in your question).
Send back a server-rendered HTML page with a <script> tag that:
Automatically saves the embedded JWT to localStorage
Automatically redirects the client to whatever page you like after that.
(Since logging in with JWTs is essentially equivalent to "saving the JWT to localStorage, my favorite option was #3, but it's possible there are downsides I haven't considered. I'm interested in hearing what others think here.)
Hope that helps!
Client: Open a popup window via $auth.authenticate('provider name').
Client: Sign in with that provider, if necessary, then authorize the application.
Client: After successful authorization, the popup is redirected back to your app, e.g. http://localhost:3000, with the code (authorization code) query string parameter.
Client: The code parameter is sent back to the parent window that opened the popup.
Client: Parent window closes the popup and sends a POST request to /auth/provider withcode parameter.
Server: Authorization code is exchanged for access token.
Server: User information is retrived using the access token from Step 6.
Server: Look up the user by their unique Provider ID. If user already exists, grab the existing user, otherwise create a new user account.
Server: In both cases of Step 8, create a JSON Web Token and send it back to the client.
Client: Parse the token and save it to Local Storage for subsequent use after page reload.
Log out
Client: Remove token from Local Storage
here is a login request from the server side. it's storing the token in the header:
router.post('/api/users/login', function (req, res) {
var body = _.pick(req.body, 'username', 'password');
var userInfo;
models.User.authenticate(body).then(function (user) {
var token = user.generateToken('authentication');
userInfo = user;
return models.Token.create({
token: token
});
}).then(function (tokenInstance) {
res.header('Auth', tokenInstance.get('token')).json(userInfo.toPublicJSON());
}).catch(function () {
res.status(401).send();
});
});
here is the login request on the react side, where I am grabbing the token from the header and setting the token in local storage once the username and password pass authentication:
handleNewData (creds) {
const { authenticated } = this.state;
const loginUser = {
username: creds.username,
password: creds.password
}
fetch('/api/users/login', {
method: 'post',
body: JSON.stringify(loginUser),
headers: {
'Authorization': 'Basic'+btoa('username:password'),
'content-type': 'application/json',
'accept': 'application/json'
},
credentials: 'include'
}).then((response) => {
if (response.statusText === "OK"){
localStorage.setItem('token', response.headers.get('Auth'));
browserHistory.push('route');
response.json();
} else {
alert ('Incorrect Login Credentials');
}
})
}
When you get a token from any passport authentication sites you have to save the token in your browser's localStorage. The Dispatch is Redux's Middleware. Ignore dispatch if you don't use redux in your app. you can just use setState here (A bit weird without redux).
Client-side:
Here's something similar API of mine, which returns token.
saving tokens
axios.post(`${ROOT_URL}/api/signin`, { email, password })
.then(response => {
dispatch({ type: AUTH_USER }); //setting state (Redux's Style)
localStorage.setItem('token', response.data.token); //saving token
browserHistory.push('/home'); //pushes back the user after storing token
})
.catch(error => {
var ERROR_DATA;
try{
ERROR_DATA = JSON.parse(error.response.request.response).error;
}
catch(error) {
ERROR_DATA = 'SOMETHING WENT WRONG';
}
dispatch(authError(ERROR_DATA)); //throw error (Redux's Style)
});
So When you make some authenticated requests,you have to attach the token with the request in this form.
authenticated requests
axios.get(`${ROOT_URL}/api/blog/${blogId}`, {
headers: { authorization: localStorage.getItem('token') }
//take the token from localStorage and put it on headers ('authorization is my own header')
})
.then(response => {
dispatch({
type: FETCH_BLOG,
payload: response.data
});
})
.catch(error => {
console.log(error);
});
Here's my index.js:
The token is checked each and everytime, so even if the browser got refreshed, you can still set the state.
checks if the user is authenticated
const token = localStorage.getItem('token');
if (token) {
store.dispatch({ type: AUTH_USER })
}
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={App}>
..
..
..
<Route path="/blog/:blogid" component={RequireAuth(Blog)} />
//ignore this requireAuth - that's another component, checks if a user is authenticated. if not pushes to the index route
</Route>
</Router>
</Provider>
, document.querySelector('.container'));
All that dispach actions does is it sets the state.
my reducer file(Redux only) else you can just use setState() in your index route file to provide the state to the whole application. Every time the dispatch is called, it runs a similar reducer file like this which sets the state.
setting the state
import { AUTH_USER, UNAUTH_USER, AUTH_ERROR } from '../actions/types';
export default function(state = {}, action) {
switch(action.type) {
case AUTH_USER:
return { ...state, error: '', authenticated: true };
case UNAUTH_USER:
return { ...state, error: '', authenticated: false };
case AUTH_ERROR:
return { ...state, error: action.payload };
}
return state;
} //you can skip this and use setState() in your index route instead
Delete the token from your localStorage to logout.
caution: Use any different name rather than token to save the token in your browser's localStorage
Server-Side:
considering your passport services file. You must set the header search.
Here's passport.js
const passport = require('passport');
const ExtractJwt = require('passport-jwt').ExtractJwt;
const JwtStrategy = require('passport-jwt').Strategy;
..
..
..
..
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromHeader('authorization'), //client's side must specify this header
secretOrKey: config.secret
};
const JWTVerify = new JwtStrategy(jwtOptions, (payload, done) => {
User.findById(payload._id, (err, user) => {
if (err) { done(err, null); }
if (user) {
done(null, user);
} else {
done(null, false);
}
});
});
passport.use(JWTVerify);
In my router.js
const passportService = require('./services/passport');
const requireAuthentication = passport.authenticate('jwt', { session: false });
..
..
..
//for example the api router the above react action used
app.get('/api/blog/:blogId', requireAuthentication, BlogController.getBlog);