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.
Related
I am attempting to setup authentication in my NextJS project and I am using Next-Auth. I am currently trying to setup a simple GET /me route that would be hit through React Query using the access_token retrieved by a successful Auth0 session Login.
BUT: the access_token received form Next-Auth w/ Auth0 useSession() is malformed
EDIT: I think the issue is that next-auth / auth0 is storing the token as an encrypted JWE. I need to figure out how to decrypt this and pass it to my api
https://github.com/nextauthjs/next-auth/issues/243
https://github.com/nextauthjs/next-auth/pull/249
https://github.com/nextauthjs/next-auth/discussions/5214
FRONTEND
in my pages > api > auth > [...nextauth].js I have the following configuration
const authOptions = {
providers: [
Auth0Provider({
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
issuer: process.env.AUTH0_ISSUER,
idToken: true,
}),
],
// Configure callbacks 👉 https://next-auth.js.org/configuration/callbacks
callbacks: {
// The JWT callback is called any time a token is written to
jwt: ({ token, user, account, profile, isNewUser }) => {
if (account) {
token.access_token = account.access_token;
token.id_token = account.id_token;
token.auth0_id = token.sub;
token.type = account.token_type;
}
delete token.name;
delete token.picture;
delete token.sub;
return token;
},
// The session callback is called before a session object is returned to the client
session: ({ session, user, token }) => {
const newSession = {
user: {
auth0_id: token.auth0_id,
email: token.email,
},
token: {
access_token: token.access_token,
id_token: token.id_token,
token_type: token.type,
},
};
return newSession;
},
},
secret: process.env.NEXTAUTH_SECRET,
};
export default NextAuth(authOptions);
Auth0 Config
in Auth0 Dashboard: Auth0 > Applications > Applications > <PROJECT_NAME> > AdvancedSettings > OAuth the signature algorithm is RS256
Successful Login Landing Page
here I am using const { data: session, status } = useSession(); to extract the value of the current session (which matches the shape created in the session callback of pages > api > auth > [...nextauth].js -- and has the access_token)
_app.jsx component
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</SessionProvider>
);
}
API Requests (from the frontend)
for each API request to the server I am setting the headers as such
if (token) headers["authorization"] = `Bearer ${token}`;
headers["Content-Type"] = "application/json";
Server Middleware
I created an auth middleware function that serves 2 purposes.
Validate the JWT passed to the route (🚨 THIS IS WHERE THINGS BREAK 🚨)
Attempt to find a user in my postgres DB with matching auth0_id (auth0|)
below is the auth middleware
// Auth0
import { isPublicRoute } from "../services/auth0/index.js";
import { expressjwt } from "express-jwt";
import jwks from "jwks-rsa";
// 👀 I have copied this directly from the Auth0 Dashboard: Applications > APIs > QuickStart
// 🚨 the express-jwt library is failing - error below
const validator = expressjwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: "https://dev-ikfyop4g.us.auth0.com/.well-known/jwks.json",
}),
audience: "thunderbolt",
issuer: "https://dev-ikfyop4g.us.auth0.com/",
algorithms: ["RS256"],
});
// NOTE: 👀 we are not actually getting to this function
// This function will retrieve the user and feed it into the request
// into a populated user model, if the id is not in the database it
// will create a new user and pull the base data from auth0
const userInjector = async (req, res, next) => {
if (isPublicRoute(req)) return next();
if (!req.auth0?.sub) throw badImplementation("JWT missing in userInjector");
req.user = await userFromReq(req);
console.log("THE USER INJECTOR RESULT :: req.user", req.user);
next();
};
const auth = () => {
return [validator, userInjector];
};
export default auth;
Inside my server I am importing this auth middleware function and using like this
server.use(auth());
THE express-jwt ERROR
UnauthorizedError: jwt malformed
at new UnauthorizedError (/Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/errors/UnauthorizedError.js:22:28)
at /Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/index.js:133:35
at step (/Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/index.js:33:23)
at Object.next (/Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/index.js:14:53)
at fulfilled (/Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/index.js:5:58)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
The access_token that I am receiving from Auth0 is malformed. Upon further investigation I would agree. The AccessToken I am receiving (as a JWT) has 5 parts (parts are strings separated by a period ..
Access Token From Auth0 Session
eyJhbGc<REMOVED_CHUNK>uYXV0aDAuY29tLyJ9.. // 2 period w/ nothing inbetween??
zOKQ2<REMOVED_CHUNK>rSm6.
9qbS5yndGKkrZ9Tc_dL8ZOuHtp_-e58uGqvGHgcpcFS8-s6SEHJYZ0_g7Ii7aYQe4AdbeK9ekW-704X_6C1r5JH3-9yBz<REMOVED_CHUNK>6Rn3Q8U0YC_x8Vp9pF_EA4GHjevXrh3HFBzCY4AEAx-Rmnzk4tZDgk3oU2rsY1NleMTwpIj0h29KIsukg113uMt5KCWKVnosSI-psaBu<REMOVED_CHUNK>lf0R_y5ClcXF6XY0ezIvuwoSQOmhulMlPsTxzBVGeoIhsooNntgAc4s.
ojmkoO_CO<REMOVED_CHUNK>URg
If everything is configured for RS256 why is Next-Auth w/ Auth0 sending me a malformed JWT.
EDIT: a 5 part JWT is a JSON Web Encryption (JWE) token...?
I have a small MEAN stack running (Angular in Frontend and NodeJS in Backend). The Frontend is protected by MSAL (#azure/msal-angular).
This part is working fine. The user gets authorized for the frontend and Angular is able to request data from MS Graph (the msal interceptor adds the token to all requests to the MS Graph and the backend):
app.module.ts
MSalModule.forRoot( new PublicClientApplication({ // MSAL Configuration
auth: {
clientId: environment.aad_client_id,
authority: 'https://login.microsoftonline.com/' + environment.aad_tenant_id + '/',
redirectUri: window.location.origin,
},
cache: {
cacheLocation : BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: isIE,
}
}), {
// MSAL Guard Configuration
interactionType: InteractionType.Redirect,
authRequest: {
scopes: ['user.read', environment.aad_scope_api]
}
}, {
// MSAL Interceptor Configuration
interactionType: InteractionType.Redirect,
protectedResourceMap: new Map([
['https://graph.microsoft.com/v1.0', ['user.read']],
[environment.apiUrl, [environment.aad_scope_api]],
])
})
After redirect from MS login I send a post request to my NodeJS Backend to establish a session.
The login route of the Backend should extract the token from the header, and send some request to the graph, to store the user details from there in the user session.
login.js
router.post('/login', (req, res) => {
if (req.session.user) {
res.json(req.session.user);
} else {
fetchUser(req, mongodb).then(result => {
req.session.user = result;
res.json(result);
}).catch(err => {
res.status(401).json(err);
})
}
});
...
async function fetchUser(token) {
try {
const token = req.headers.authorization;
request({
headers: { 'Authorization': token },
uri: 'https://graph.microsoft.com/v1.0/me',
method: 'GET'
}, { json: true }, (err, res, body) => {
if (err) { throw err; }
const obj = ...do some things
return obj;
});
} catch(err) {
throw err;
}
}
The issue is, that the token is only valid from Frontend. MS recommend the on-behalf-of-flow for that, but I'm not able to find any way to solve this. So how can I request a new token for my backend?
You can request a token for the backend to access Graph using the client credentials authentication, and set the scopes for Graph as Application Permissions on the App Registration, such as User.Read.All.
You would instead read the "oid" from the AAD access token passed from frontend to backend for discovering the user for formatting requests to Graph. Microsoft created a tutorial on implementing which you may find helpful.
I am working on my second React/Redux application (I'm new to Redux) and also the first MERN stack app altogether.
Currently, I am having an Authentication problem on the Front-End side in React/Redux Which goes like this:
I have established a protect middleware in my Back-End to check if the user which logged in is always logged in with valid JWT-Token or not by setting the Token inside the Browser Cookie with HttpOnly flag enabled {no secure flag yet but I will enable that too in production step}
The main problem is that once I log in the application, everything is fine and if the user is validated without any errors then inside the authReducer, the isAuthenticated property will be set to true and then logged in user's data is passed to the redux store to be used. but once I redirect to the main feed page of the app everything is gone the isAuthenticated is now false and user data is null so the app crashes.
I know that I must call the auth protected route every time I send a request to the server but I am stuck because it needs some piece of the logged-in user like username or ID to be sent along with the each request to validate it and I can't store them in local storage because of safety issues. { storing sensitive data in local storage is not a good practice}
this is log in function in Redux Actions:
export const loginUser = (loginData) => async (dispatch) => {
try{
setLoading();
const res = await axios({
method: "post",
url: "/users/login",
data: {loginData}
});
dispatch({
type: TYPES.LOGIN_SUCCESS,
payload: {
status: res.data.status,
token: res.data.token,
user: res.data.data.user
}
});
// This below code will set the logged in user after successful login
getLoggedInUser(res.data.data.user.username);
}catch(error){
dispatch({
type: TYPES.LOGIN_FAIL,
payload: {
status: error.response.data.status,
message: error.response.data.message
}
});
}
};
as you see after the successful log in, the token and user data is dispatched to store but after component reload they are gone so I can't re-Authenticate the logged in user.
this is my getLoggedInUser redux action function:
export const getLoggedInUser = (userName) => async (dispatch) => {
try{
setLoading();
const res = await axios.get(`/auth/${userName}`, {
// headers:{
// Cookies: `jwt=${token}`
// }
});
dispatch({
type: TYPES.GET_LOGGED_IN_USER,
payload: res.data.data.loggedInUser
});
}catch(error){
dispatch({
type: TYPES.AUTH_ERROR,
payload: {
status: error.response.data.status,
message: error.response.data.message
}
});
}
};
Any solutions?
I am trying to connect to the G-Suite's User directory using the google-admin-sdk. I am using an API Key for authorization and I am not able to reach a successful execution.
Here is the code snippet that I'm using:
import { google } from 'googleapis';
import uuid from 'uuid/v4';
const API_KEY = 'my api key goes here';
google.admin({
version: 'directory_v1',
auth: API_KEY
}).users.list({
customer: 'my_customer',
maxResults: 10,
orderBy: 'email',
}, (err, res: any) => {
if (err) { return console.error('The API returned an error:', err.message); }
const users = res.data.users;
if (users.length) {
console.log('Users:');
users.forEach((user: any) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log('No users found.');
}
});
Output:
Login Required
Can someone tell me what I am doing wrong here?
Also, how do I proceed further for listening to the events emitted by the Google API?
---UPDATE---
Here is the snippet that works for me now:
import { JWT } from 'google-auth-library';
import { google } from 'googleapis';
// Importing the serivce account credentials
import { credentials } from './credentials';
const scopes = ['https://www.googleapis.com/auth/admin.directory.user'];
const adminEmail = 'admin_account_email_address_goes_here';
const myDomain = 'domain_name_goes_here';
async function main () {
const client = new JWT(
credentials.client_email,
undefined,
credentials.private_key,
scopes,
adminEmail
);
await client.authorize();
const service = google.admin('directory_v1');
const res = await service.users.list({
domain: myDomain,
auth: client
});
console.log(res);
}
main().catch(console.error);
--- Bonus Tip ---
If you face any Parse Errors while using other methods of the directory, remember to JSON.stringify the request body. For example, on the admin.users.watch method:
// Watch Request
const channelID = 'channel_id_goes_here';
const address = 'https://your-domain.goes/here/notifications';
const ttl = 3600; // Or any other TTL that you can think of
const domain = 'https://your-domain.goes';
const body = {
id: channelID,
type: 'web_hook',
address,
params: {
ttl,
},
};
// Remember to put this in an async function
const res = await service.users.watch({
domain,
customer: 'my_customer',
auth: client, // get the auth-client from above
event: 'add'
}, {
headers: {
'Content-Type': 'application/json'
},
// This is the important part
body: JSON.stringify(body),
});
As you can see in the official documentation, every request sent "to the Directory API must include an authorization token". In order to authorize your request, you have to use OAuth 2.0.
You are providing an API key instead, which is not appropriate for this process. API keys are usually used for accessing public data, not users' private data as in your current situation.
You should follow the steps provided in the Node.js Quickstart instead:
First, obtain client credentials from the Google API Console.
Second, authorize the client: obtain an access token after setting the user credentials and the appropriate scopes (a process accomplish in functions authorize and getNewToken in the Quickstart).
Finally, once the client is authorized, call the API (function listUsers).
Update:
If you want to use a Service Account for this, you will have to follow these steps:
Grant domain-wide delegation to the Service Account by following the steps specified here.
In the Cloud console, create a private key for the Service Account and download the corresponding JSON file. Copy it to your directory.
Use the Service Account to impersonate a user who has access to this resource (an Admin account). This is achieved by indicating the user's email address when creating the JWT auth client, as indicated in the sample below.
The code could be something along the following lines:
const {google} = require('googleapis');
const key = require('./credentials.json'); // The name of the JSON you downloaded
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
['https://www.googleapis.com/auth/admin.directory.user'],
"admin#domain" // Please change this accordingly
);
// Create the Directory service.
const service = google.admin({version: 'directory_v1', auth: jwtClient});
service.users.list({
customer: 'my_customer',
maxResults: 10,
orderBy: 'email',
}, (err, res) => {
if (err) return console.error('The API returned an error:', err.message);
const users = res.data.users;
if (users.length) {
console.log('Users:');
users.forEach((user) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log('No users found.');
}
});
Reference:
Directory API: Authorize Requests
Directory API: Node.js Quickstart
Delegate domain-wide authority to your service account
Google Auth Library for Node.js
I hope this is of any help.
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
}
}
}