I'm following this tutorial to implement jwt authentication in hapijs v17.2.
I did everything according to the tutorial, but the following error is driving me crazy, even debugging didn't make any change.
error
Debug: internal, implementation, error
TypeError: cb is not a function
at Object.secretProvider [as key] (C:\Users\user\WebstormProjects\hapi-blog\node_modules\jwks-rsa\lib\integrations\hapi.js:30:14)
at Object.authenticate (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi-auth-jwt2\lib\index.js:123:87)
at module.exports.internals.Manager.execute (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi\lib\toolkit.js:35:106)
at module.exports.internals.Auth._authenticate (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi\lib\auth.js:242:58)
at authenticate (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi\lib\auth.js:217:21)
at module.exports.internals.Request._lifecycle (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi\lib\request.js:261:62)
at <anonymous>
app.js
const hapi = require('hapi');
const mongoose = require('./db');
const hapi_auth_jwt = require('hapi-auth-jwt2');
const jwksa_rsa = require('jwks-rsa');
const dog_controller = require('./controllers/dog');
const server = new hapi.Server({
host: 'localhost',
port: 4200
});
const validate_user = (decoded, request, callback) => {
console.log('Decoded', decoded);
if (decoded && decoded.sub) {
return callback(null, true, {});
}
return callback(null, true, {});
};
const register_routes = () => {
server.route({
method: 'GET',
path: '/dogs',
options: {
handler: dog_controller.list,
auth: false
}
});
// Test
server.route({
method: 'POST',
path: '/a',
options: {
handler: (req, h) => {
return h.response({message: req.params.a});
},
auth: false
}
});
server.route({
method: 'GET',
path: '/dogs/{id}',
options: {
handler: dog_controller.get
}
});
server.route({
method: 'POST',
path: '/dogs',
options: {
handler: dog_controller.create
}
});
server.route({
method: 'PUT',
path: '/dogs/{id}',
handler: dog_controller.update
});
server.route({
method: 'DELETE',
path: '/dogs/{id}',
handler: dog_controller.remove
});
};
const init = async () => {
await server.register(hapi_auth_jwt);
server.auth.strategy('jwt', 'jwt', {
key: jwksa_rsa.hapiJwt2Key({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
// YOUR-AUTH0-DOMAIN name e.g https://prosper.auth0.com
jwksUri: 'https://mrvar.auth0.com/.well-known/jwks.json'
}),
verifyOptions: {
audience: 'https://mrvar.auth0.com/api/v2/',
issuer: 'https://mrvar.auth0.com',
algorithm: ['RS256']
},
validate: validate_user
});
server.auth.default('jwt');
// Register routes
register_routes();
// Start server
await server.start();
return server;
};
init().then(server => {
console.log('Server running at: ', server.info.uri);
}).catch(err => {
console.log(err);
});
When I make a request to routes with auth: false, the handler works properly then I get the expected result, but requests to routes without auth return the following json :
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "An internal server error occurred"
}
More info:
node version: 8.9.4
npm version: 5.6.0
hapi version: 17.2.0
hapi-auth-jwt2: github:salzhrani/hapi-auth-jwt2#v-17
jwks-rsa: 1.2.1
mongoose: 5.0.6
nodemon: 1.15.0
Both libraries has support for hapi v.17
I also faced this issue, but surprisingly both libraries has support for hapi v.17, but all documentation is based on old versions or didn't use this combination ;)
How to use it
There are few things that has to be changed then using hapijs v.17
Verify that you're using libraries versions which support hapijs 17:
"jwks-rsa": "^1.3.0",
"hapi-auth-jwt2": "^8.0.0",
Use hapiJwt2KeyAsync instead of hapiJwt2Key.
Information about this new async method is hidden in node-jwks-rsa package documentation
validate function has now new contract
Please change your existing validate function to below type:
async (decoded: any, request: hapi.Request): {isValid: boolean, credentials: {}}
Working example (with handled passing scopes)
const validateUser = async (decoded, request) => {
if (decoded && decoded.sub) {
return decoded.scope
? {
isValid: true,
credentials: {
scope: decoded.scope.split(' ')
}
}
: { isValid: true };
}
return { isValid: false };
};
server.auth.strategy('jwt', 'jwt', {
complete: true,
key: jwksRsa.hapiJwt2KeyAsync({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: env.auth.jwksUri
}),
verifyOptions: {
audience: '/admin',
issuer: env.auth.issuer,
algorithms: ['RS256']
},
validate: validateUser
});
The validate function changed in hapi#17 to not have a callback function. Based on your example it should now look something like this:
const validate = async (decoded, request) => {
if (decoded && decoded.sub) {
return { isValid: true };
}
return { isValid: false };
};
Part of the returned object can also include credentials which would represent the user that is authenticated and you can also do a scope as part of the credentials.
Then if you want to you can access the credentials as part of the request object like request.auth.credentials
Related
I was going through the next-auth documentation but didn't find any mention of connecting to custom configured Redis without the use of Upstash for a persistent session store.
My use case is straightforward. I am using Nginx as a load balancer between multiple nodes for my nextJS application and I would like to persist the session if in case the user logs in and refreshes the page as Nginx switches between nodes.
For e.g My Nginx config
server {
listen 80;
server_name _;
location / {
proxy_pass http://backend;
}
}
upstream backend {
ip_hash;
server <nextjs_app_ip_1>:8000;
server <nextjs_app_ip_2>:8000;
}
As you can see from the example Nginx config, there are multiple upstream server pointers here that require user session persistence.
I am using the credentials provider of next-auth as I have a Django-based auth system already available.
I did see the implementation of the next-auth adapter with Upstash. However, I have my own custom server running with Redis.
I tried connecting to Redis using ioredis which works fine as it is connected. However, I am not sure how can I use Redis here with next-auth to persist session and validate at the same time?
For e.g In express, you have a session store which you can pass your Redis Client with and it should automatically take care of persistence. Is there anything I can do to replicate the same behavior in my case?
For e.g In Express
App.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'secret$%^134',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // if true only transmit cookie over https
httpOnly: false, // if true prevent client side JS from reading the cookie
maxAge: 1000 * 60 * 10 // session max age in miliseconds
}
}))
My Code:
import CredentialsProvider from "next-auth/providers/credentials";
import {UpstashRedisAdapter} from "#next-auth/upstash-redis-adapter";
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL); //points to my custom redis docker container
export const authOptions = {
providers: [CredentialsProvider({
name: 'auth',
credentials: {
email: {
label: 'email',
type: 'text',
placeholder: 'jsmith#example.com'
},
password: {
label: 'Password',
type: 'password'
}
},
async authorize(credentials, req) {
const payload = {
email: credentials.email,
password: credentials.password
};
const res = await fetch(`my-auth-system-url`, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json'
}
});
const user = await res.json();
console.log("user", user);
if (!res.ok) {
throw new Error(user.exception);
}
// If no error and we have user data, return it
if (res.ok && user) {
return user;
}
// Return null if user data could not be retrieved
return null;
}
})],
adapter: UpstashRedisAdapter(redis),
pages: {
signIn: '/login'
},
jwt: {
secret: process.env.SECRET,
encryption: true
},
callbacks: {
jwt: async({token, user}) => {
user && (token.user = user)
return token
},
session: async({session, token}) => {
session.user = token.user
return session
},
async redirect({baseUrl}) {
return `${baseUrl}/`
}
},
session: {
strategy: "jwt",
maxAge: 3000
},
secret: process.env.SECRET,
debug: true
}
export default NextAuth(authOptions)
Thank you so much for the help.
Now I'm doing an authentication project to learn how to integrate with the FE and GraphQL to store the refresh token inside the cookies and use the access token to get the information. Unfortunately I was unable to store the refresh token after I clicked the login button and the cors error even i'm following all the details and steps from the official website. Thank you everyone for being attention on it
Server Code
async function startApolloServer() {
const app = express();
app.use(cors({
origin:'*',
credentials:true, //access-control-allow-credentials:true
}))
const httpServer = http.createServer(app);
app.use(cookiesParser());
app.post("/refresh_token", async (req: Request, res: Response) => {
console.log(req.cookies);
const token = req.cookies.jid;
if (!token) {
return res.send({ status: false, accessToken: "" });
}
let payload: any = null;
try {
payload = verify(token, process.env.SECRET_KEY as string);
} catch (error) {
return res.send({ status: false, accessToken: "" });
}
// token is valid, find user and send back accessToken
const user: any = await AppDataSource.manager.getRepository(User).findOne({
where: {
id: payload.userId,
},
});
if (!user) {
return res.send({ status: false, accessToken: "" });
}
if (user.tokenVersion !== payload.tokenVersion) {
return res.send({ status: false, accessToken: "" });
}
sendRefreshToken(res, createRefreshToken(user));
return res.send({ status: true, accessToken: createAccessToken(user) });
});
const schema = await buildSchema({
resolvers: [UserQuery, UserMutation],
});
await AppDataSource.initialize();
const server = new ApolloServer({
schema,
context: ({ req, res }) => {
return { req, res };
},
csrfPrevention: true,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
server.applyMiddleware({ app });
await new Promise<void>((resolve) =>
httpServer.listen({ port: 4000 }, resolve)
);
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
}
Frontend Code
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
cache: new InMemoryCache(),
credentials:'include'
});
Error
Error Image
Remove * and use
app.use(cors())
If you want to allow to access backend can be accessed from anywhere. Or can use
var corsOptions = {
origin: 'http://example.com',
}
If you allow any specific domain. For more options see the documentation.
I have login route, that return user:
#Post('login')
async login(#Body() user: LoginRequest, #Res() reply): Promise<User> {
const foundUser = await this.authService.validateUser(user.email, user.password);
reply.setCookie('t', foundUser._id, { path: '/' });
// reply.send(foundUser);
return foundUser;
}
My problem is that it is not return nothing (stuck on pending...)), unless I do reply.send(foundUser);
I'm using a proxy with allow cors origin:
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
// enable cors for static angular site.
const corsOptions = {
origin: 'http://localhost:4200',
optionsSuccessStatus: 200,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
};
app.register(require('fastify-cors'), corsOptions);
// enable cookie for auth.
app.register(require('fastify-cookie'));
// validate types and extra
app.useGlobalPipes(new ValidationPipe({ forbidUnknownValues: true }));
await app.listen(3000);
}
Link to the source code.
You have to remove #Res() reply from your controller function. As soon as you inject the response object, a lot of nest features stop working e.g. Interceptors.
#Post('login')
async login(#Body() user: LoginRequest): Promise<User> {
return this.authService.validateUser(user.email, user.password);
}
You could use an interceptor to set the cookie dynamically instead.
I try to learn from an example to use express together with handlebars on firebase.
For the express way, we can send the "app" instance directly to the "functions.https.onRequest" like...
const app = express();
...
app.get('/', (req, res) => {
...
});
exports.app = functions.https.onRequest(app);
See live functions
As my understanding it's working because "express" act like http-node, so it can respond "http plain".
Comparing to hapi, here is hello-world
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({
host: 'localhost',
port: 8000
});
server.route({
method: 'GET',
path:'/hello',
handler: function (request, reply) {
return reply('hello world');
}
});
server.start((err) => {
console.log('Server running at:', server.info.uri);
});
From the hapi example, is it possible to use hapi on firebase cloud function?
Can I use hapi without starting a server like express?
This code was straight forward as i mixed the express API that used by Firebase with hapijs API, thanks to the blog given by mister #dkolba
You can ivoke the url hapijs handler by going to
http://localhost:5000/your-app-name/some-location/v1/hi
example: http://localhost:5000/helloworld/us-central1/v1/hi
const Hapi = require('hapi');
const server = new Hapi.Server();
const functions = require('firebase-functions');
server.connection();
const options = {
ops: {
interval: 1000
},
reporters: {
myConsoleReporter: [{
module: 'good-squeeze',
name: 'Squeeze',
args: [{ log: '*', response: '*' }]
}, {
module: 'good-console'
}, 'stdout']
}
};
server.route({
method: 'GET',
path: '/hi',
handler: function (request, reply) {
reply({data:'helloworld'});
}
});
server.register({
register: require('good'),
options,
}, (err) => {
if (err) {
return console.error(err);
}
});
// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions
exports.v1 = functions.https.onRequest((event, resp) => {
const options = {
method: event.httpMethod,
url: event.path,
payload: event.body,
headers: event.headers,
validate: false
};
console.log(options);
server.inject(options, function (res) {
const response = {
statusCode: res.statusCode,
body: res.result
};
resp.status(res.statusCode).send(res.result);
});
//resp.send("Hellworld");
});
Have a look at the inject method (last code example): http://www.carbonatethis.com/hosting-a-serverless-hapi-js-api-with-aws-lambda/
However, I don't think that this is feasible, because you would still need to hold onto the response object of the express app instance Google Cloud Functions provide to http triggered functions, as only send(), redirect() or end() will work to respond to the incoming request and not hapi's methods (see https://firebase.google.com/docs/functions/http-events).
A few changes are required in order to get compatible with hapijs 18.x.x
'use strict';
const Hapi = require('#hapi/hapi')
, functions = require('firebase-functions');
const server = Hapi.server({
routes: {cors: true},
});
server.register([
{
plugin: require('./src/plugins/tools'),
routes: {
prefix: '/tools'
}
},
]);
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
return 'It worlks!';
}
});
exports.v1 = functions.https.onRequest((event, resp) => {
//resp: express.Response
const options = {
method: event.httpMethod,
headers: event.headers,
url: event.path,
payload: event.body
};
return server
.inject(options)
.then(response => {
delete response.headers['content-encoding']
delete response.headers['transfer-encoding']
response.headers['x-powered-by'] = 'hapijs'
resp.set(response.headers);
return resp.status(response.statusCode).send(response.result);
})
.catch(error => resp.status(500).send(error.message || "unknown error"));
});
I have a route as below which serves the static pages:
{
method: 'GET',
path: '/webapp/{param*}',
config: {
handler: {
directory :{
path : Path.join(__dirname, '../../webapp/'),
index: true
}
}
}
}
So, I want to check if the user is logged in or not before it takes user to that url "/webapp/#blabla".
User Can only access that url if user is logged in.
I tried to add pre option with a function in the above route but no luck.
{
method: 'GET',
path: '/webapp/{param*}',
pre:[{method:checkUrl, assign:'m1'}],
config: {
handler: {
directory :{
path : Path.join(__dirname, '../../webapp/'),
index: true
}
}
}
}
And the checkUrl function is as:
var checkUrl = function(request, reply) {
if (!request.auth.isAuthenticated) {
// redirect to login page
reply.redirect('/login');
}
return true;
}
Why is that i cannot get redirected to login page?
It depends slightly on which auth scheme you're using but the same principle applies. Here's an example using hapi-auth-basic (adapted from the example in the README):
var Bcrypt = require('bcrypt');
var Hapi = require('hapi');
var Path = require('path');
var Inert = require('inert');
var server = new Hapi.Server();
server.connection({ port: 4000});
var users = {
john: {
username: 'john',
password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm', // 'secret'
name: 'John Doe',
id: '2133d32a'
}
};
var validate = function (request, username, password, callback) {
var user = users[username];
if (!user) {
return callback(null, false);
}
Bcrypt.compare(password, user.password, function (err, isValid) {
callback(err, isValid, { id: user.id, name: user.name });
});
};
server.register([
require('inert'),
require('hapi-auth-basic')
], function (err) {
server.auth.strategy('simple', 'basic', { validateFunc: validate });
server.route({
method: 'GET',
path: '/webapp/{param*}',
config: {
auth: 'simple', // THIS IS THE IMPORTANT BIT
handler: {
directory :{
path : Path.join(__dirname, 'files'),
index: true
}
}
}
});
server.start(function (err) {
if (err) {
throw err;
}
console.log('Server started!');
})
});
The important point is just to add an auth property to the route config with the strategy name. It's the same as you would do for any routes. Have a read of this tutorial, it might clear it up for you.
Are you able to adapt that to your needs?