Cookie does not appear to be sent via fetch or hapi server is unable to receive cookie - fetch-api

So I have a simple backend server created with Hapi API and the frontend I'm using fetch. These are on different ports so I have CORs enabled and all the sweet stuff. I'm currently trying to set a refresh token in the browser using a http only cookie. As far as I can verify, the http only cookie is being set in the browser when login function is completed. I'm currently trying to send the http only cookie back to the server so I can set up the refresh token route and I can't seem to send or even verify that http token is sent back to the server.
Here's the server setting.
"use strict";
require("dotenv").config();
const Hapi = require("#hapi/hapi");
const Jwt = require("#hapi/jwt");
const routes = require("./routes/routes");
exports.init = async () => {
const server = Hapi.server({
port: 3000,
host: "localhost",
routes: {
cors: {
origin: ["*"],
credentials: true,
},
},
});
require("./models");
await server.register(Jwt);
server.auth.strategy("jwt", "jwt", {
keys: { key: process.env.SECRET_KEY, algorithms: ["HS256"] },
verify: { aud: false, iss: false, sub: false, exp: true },
validate: false,
});
server.state("refresh", {
ttl: 1000 * 60 * 60 * 24,
isSecure: true,
isHttpOnly: true,
encoding: "base64json",
clearInvalid: true,
strictHeader: true,
isSameSite: "None",
});
server.route(routes);
return server;
};
process.on("unhandledRejection", (err) => {
console.log(err);
process.exit(1);
});
Here's the login request and returns the http only cookie. This part works, the http cookie is returned and set.
const validateUserAndReturnToken = async (req, h) => {
const user = await User.findOne({
$or: [{ email: req.payload.username }, { username: req.payload.username }],
});
if (user) {
const match = await bcrypt.compare(req.payload.password, user.passwordHash);
if (match) {
const token = await createToken(match);
const refreshToken = await createRefreshToken(match);
h.state("refresh", refreshToken);
return { id_token: token, user: formatUser(user) };
} else {
throw boom.notAcceptable("Username and password did not match.");
}
} else {
throw boom.notAcceptable("Username or email was not found.");
}
};
Here's the fetch request I'm using to test sending a http cookie only back. I have credential: include so I don't know what is problem?
import type { DateInfo } from "#/stores/application";
const api = "http://localhost:3000/report";
let token = localStorage.getItem("user-token");
const headers = new Headers();
headers.append("Authorization", `Bearer ${token}`);
headers.append("Content-Type", "application/json");
export const getJobReport = async (dateFilter: DateInfo) => {
let response = await fetch(
`${api}/${dateFilter.startDate}/${dateFilter.endDate}`,
{
method: "GET",
headers,
credentials: "include",
}
);
return await response.json();
};
I have checked the application tab as well as the network request so I know set cookie is being sent and set on the browser. The problem is I can't seem to get the cookie back from the browser when fetch request is sent back to the server.
Here's the code I'm using to just check the existence of the cookie. According to Hapi Doc , req.state[cookie-name] which in this case is 'refresh' should have the cookie value. Refresh is returning undefined so I went up one level and check for req.state and gets an empty object {}.
route
{
method: "GET",
path: "/report/{startDate}/{endDate}",
options: {
auth: "jwt",
state: {
parse: true,
failAction: "error",
},
validate: {
params: Joi.object({
startDate: Joi.string(),
endDate: Joi.string(),
}),
},
},
handler: handlers.report.getJobApplicationReport,
},
handler
const getJobApplicationReport = async (req, h) => {
console.log("TEST", req.state);
const start = new Date(req.params.startDate);
const end = new Date(req.params.endDate);
try {
const applications = await Application.find({
dateApplied: { $gte: start, $lt: end },
});
// 'Applied', 'In Process', 'Rejected', 'Received Offer'
const total = applications.length;
let rejectedCount = 0;
let inProcessCount = 0;
applications.forEach((app) => {
if (app.status === "Rejected") {
rejectedCount++;
}
if (app.status === "In Process") {
inProcessCount++;
}
});
return {
total,
rejectedCount,
inProcessCount,
};
} catch (error) {
console.log(error);
throw boom.badRequest(error);
}
};
I've looked through all the Hapi documentation, fetch documentation and stackoverflow question/answers but can't seem to find a solution. I can't verify whether it's the fetch request that's not sending the http only cookie or the server setting that's not parsing it. Any help to determine the issue or solution would be greatly appreciated.
I've looked through all the Hapi documentation, fetch documentation and stackoverflow question/answers but can't seem to find a solution. I can't verify whether it's the fetch request that's not sending the http only cookie or the server setting that's not parsing it. Any help to determine the issue or solution would be greatly appreciated.

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.

Where do i put the user token for api request

I have this sample code to get the users albums, but where do I put the usertoken on the request.get call. I can't find anywhere online that shows where it goes.
"use strict";
const fs = require("fs");
const jwt = require("jsonwebtoken");
const request = require("request");
const privateKey = fs.readFileSync("AuthKey.p8").toString();
const jwtToken = jwt.sign({}, privateKey, { algorithm: "ES256", expiresIn: "180d", issuer: "", header: { alg: "ES256", kid: "" } });
console.log("token:", jwtToken, "\n");
var url = "";
url = "https://api.music.apple.com/v1/me/library/albums";
request.get(
{ url: url, auth: { bearer: jwtToken }, json: true }, (err, httpResponse, body) => { if (err) { console.error(err); } else { console.log(body.results.albums.data); }
} );
You can simply attach the token to the request with query parameter or in the Authorization header.
Putting it in the header is a better way.
You can read that for more info:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization

NextAuth Hasura Refresh token

I am trying to set up NextAuth for Hasura authentication and authorization. Since Hasura needs custom jwt claims I can't use the default access token provided by an OAuth provider. So I am using encode block in [...nextauth].js to encode a custom jwt token and everything works fine. But I don't know how to implement a refresh token for my custom token. Below is my "pages/api/auth/[...nextauth].js"
import * as jwt from "jsonwebtoken";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorizationUrl:
"https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code",
}),
],
secret: process.env.SECRET,
session: {
jwt: true,
},
jwt: {
secret: process.env.SECRET,
encode: async ({ secret, token, maxAge }) => {
const jwtClaims = {
sub: token.id,
name: token.name,
email: token.email,
picture: token.picture,
iat: Date.now() / 1000,
exp: Math.floor(Date.now() / 1000) + 60,
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": ["user"],
"x-hasura-default-role": "user",
"x-hasura-role": "user",
"x-hasura-user-id": token.id,
},
};
const encodedToken = jwt.sign(jwtClaims, secret, { algorithm: "HS256" });
return encodedToken;
},
decode: async ({ secret, token, maxAge }) => {
const decodedToken = jwt.verify(token, secret, { algorithms: ["HS256"] });
return decodedToken;
},
},
pages: {
// signIn: '/auth/signin', // Displays signin buttons
// signOut: '/auth/signout', // Displays form with sign out button
// error: '/auth/error', // Error code passed in query string as ?error=
// verifyRequest: '/auth/verify-request', // Used for check email page
// newUser: null // If set, new users will be directed here on first sign in
},
// Callbacks are asynchronous functions you can use to control what happens
// when an action is performed.
// https://next-auth.js.org/configuration/callbacks
callbacks: {
// async signIn(user, account, profile) { return true },
// async redirect(url, baseUrl) { return baseUrl },
async session(session, token) {
const encodedToken = jwt.sign(token, process.env.SECRET, {
algorithm: "HS256",
});
session.token = encodedToken;
session.id = token.id;
return Promise.resolve(session);
},
async jwt(token, user, account, profile, isNewUser) {
const isUserSignedIn = user ? true : false;
// make a http call to our graphql api
// store this in postgres
if (isUserSignedIn) {
token.id = profile.id.toString();
}
return Promise.resolve(token);
},
},
// Events are useful for logging
// https://next-auth.js.org/configuration/events
events: {},
// Enable debug messages in the console if you are having problems
debug: true,
});
Can somebody tell me how to handle refresh token with next-auth when using custom jwt tokens?
I'm currently using a method I found on a post here on the next-auth issues page.
Essentially, we're using a combination of the clientMaxAge option you can pass into the provider to refetch the session, which reruns the jwt callback. I'm not sure I'm using the keepAlive property correctly, but this seems to poll correctly at the moment, though you may need to experiment with this.
Inside your JWT callback, you can have your logic that will check your existing expiry time against another, and fetch a new token from your server to assign for the session.
//_app.tsx
const sessionOptions = {
clientMaxAge: 60 * 30, // Re-fetch session if cache is older than 30 minutes
keepAlive: 60 * 30, // Send keepAlive message every hour
};
<Provider options={sessionOptions} session={pageProps.session}>
..
</Provider>
// [...nextauth].ts
const callbacks: CallbacksOptions = {
async jwt(token: any, user: any) {
if (user) {
token.accessToken = user.token;
token.expires = Date.now() + user.config.USER_SESSION_LENGTH_IN_SECONDS * 1000;
}
// Don't access user as it's only available once, access token.accessToken instead
if (token?.accessToken) {
const tokenExpiry = token.expires;
const almostNow = Date.now() + 60 * 1000;
if (tokenExpiry !== undefined && tokenExpiry < almostNow) {
// Token almost expired, refresh
try {
const newToken = await api.renewToken(token.accessToken); // calling external endpoint to get a new token
// re-assign to the token obj that will be passed into the session callback
token.accessToken = newToken.token;
token.expires = Date.now() + user.config.USER_SESSION_LENGTH_IN_SECONDS * 1000;
} catch (error) {
console.error(error, 'Error refreshing access token');
}
}
}
return token;
},
async session(session: any, user: any) {
session.accessToken = user.accessToken;
session.expires = user.expires;
return session;
}
}

keycloak logout doesn't invalidate token when call a rest api

I've a React app that uses Keycloak as a authentication service. Also I've a Nodejs rest api with endpoints secured by keycloak, so the React app sends JWT when needs call an api. In Keycloak admin console I created 1 public client with users and roles.
All works fine, but the only problems is when a I logout through admin console, or
from my React application berfore that expiration time, I still can call to my app with these token.
Why my backend app doesn't validate the token with server?
My node app uses keycloak-node-connect adapter and my keycloak.json is:
{
"client-id": "my-public-client",
"bearer-only": true,
"auth-server-url": "http://localhost:8180/auth",
"realm": "my-realm"
}
Solved
I can solved my probleam like suggested in Keycloak: Access token validation end point
keycloak.config.js
var session = require('express-session');
var Keycloak = require('keycloak-connect');
var request = require('request');
const createError = require('http-errors');
let _keycloak;
var memoryStore = new session.MemoryStore();
function initKeycloak() {
if (_keycloak) {
console.log("Trying to init Keycloak again!");
return _keycloak;
}
else {
console.log("Initializing Keycloak...");
_keycloak = new Keycloak({ store: memoryStore });
return _keycloak;
}
}
function getKeycloak() {
if (!_keycloak) {
console.error('Keycloak has not been initialized. Please called init first.');
}
return _keycloak;
}
async function validateTokenKeycloak(req, res, next) {
if (req.kauth && req.kauth.grant) {
console.log('--- Verify token ---');
try {
var result = await _keycloak.grantManager.userInfo(req.kauth.grant.access_token);
//var result = await _keycloak.grantManager.validateAccessToken(req.kauth.grant.access_token);
if(!result) {
console.log(`result:`, result);
throw Error('Invalid Token');
}
} catch (error) {
console.log(`Error: ${error.message}`);
return next(createError.Unauthorized());
}
}
next();
}
module.exports = {
memoryStore,
initKeycloak,
getKeycloak,
validateTokenKeycloak
};
app.js
const express = require('express');
const createError = require('http-errors');
const dotenv = require('dotenv').config();
const session = require('express-session');
const keycloakConfig = require('./config/keycloak.config');
const app = express();
// Keycloak
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: true,
store: keycloakConfig.memoryStore
}));
const keycloak = keycloakConfig.initKeycloak();
app.use(keycloak.middleware());
app.use(keycloakConfig.validateTokenKeycloak);
app.use("/health", require('./routes/health.route'));
// 404 handler and pass to error handler
app.use((req, res, next) => {
next(createError(404, 'Not found'));
});
// Error Handler
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.send({
error : {
status : err.status || 500,
message : err.message
}
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server starter on port ${PORT}`);
});

My POST request works on Postman but not with axios

I have a nuxt project deployed on Netlify and now I want to add a newsletter (add a subscriber to my audience on Mailchimp). To achieve that, I've opted to use the AWS serverless lambda functions. To be honest, it's the first time that i've heard about serverless functions. I found this tutorial https://hashinteractive.com/blog/nuxt-js-mailchimp-integration-add-contact-to-list/ and at the end, i've decided to make a test on Postman. I've made a post to http://localhost:8888/.netlify/functions/subscribe and it worked. But when I try the same thing with axios I get the error 405 (method not allowed).
Newsletter.vue
<form #submit.prevent='submitNewsletter' class="newsletter__form" >
<input type="email" placeholder="E-mail" class="newsletter__form-input" v-model="email">
<button class="newsletter__form-button" type="submit">Subscribe</button>
</form>
</div>
</template>
<script>
import axios from 'axios';
export default {
data(){
return{
email: ''
}
},
methods:{
submitNewsletter(){
axios.post('http://localhost:8888/.netlify/functions/subscribe', { email: this.email}, {
headers: {
methods: 'POST',
'Content-Type':'application/json'
}
})
.then(function (response) {
console.log(response);
}).
catch((error) =>{
console.log('The error:' + error)
})
this.$toasted.success("Thank you for your subscription !!!", {
theme: "toasted-primary",
position: "top-left",
containerClass: 'myContainer',
fitToScreen: true,
fullWidth: true,
duration : 5000
});
}
}
}
</script>
functions > subscribe > subscribe.js
const fetch = require('node-fetch');
const base64 = require('base-64');
exports.handler = async (event, context) => {
// Only allow POST
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
const errorGen = msg => {
return { statusCode: 500, body: msg };
};
try {
const { email } = JSON.parse(event.body);
console.log(email);
if (!email) {
return errorGen('Missing Email');
}
const subscriber = {
email_address: email,
status: 'subscribed',
};
console.log(subscriber);
console.log(JSON.stringify(subscriber));
const creds = `blooming-thoughts:${process.env.MAILCHIMPS_API_KEY}`;
const response = await fetch(`https://us20.api.mailchimp.com/3.0/lists/${process.env.AUDIENCE_ID}/members/`, {
method: 'POST',
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
Authorization: `Basic ${base64.encode(creds)}`, },
body: JSON.stringify(subscriber),
});
const data = await response.json();
if (!response.ok) {
// NOT res.status >= 200 && res.status < 300
return { statusCode: data.status, body: data.detail };
}
return {
statusCode: 200,
body: JSON.stringify({ msg: "You've signed up to the mailing list!", detail: data, }),
};
} catch (err) {
console.log(err); // output to netlify function log
return {
statusCode: 500,
body: JSON.stringify({ msg: err.message }),
};
}
};
My netlify.toml
[build]
publish = "dist"
functions = 'functions'
I've made a push to my repository and netlify built without any error, but when I try to add a newsletter from my site nothing happens.
I've solved my problem. If you're having trouble like I was, follow these steps:
Create a folder called functions in your root directory, then create a index.js (the file name is up to you).
Create a file called netlify.toml and add the following code :
[build]
functions = "functions"
Then, inside your index.js goes the code that will communicate with your API
On the vue component, make a post request to the <your-site>/.netlify/functions/index . You can find the endpoint going to functions on netlify dashbord.
Don't forget to register your env variables on Netlify (Build & Deploy) and that's it.