http request with status 200 but no response data - node.js

I am trying to make an http request on refresh within my Angular frontend to a nodejs backend and expect to receive a token as response. Sometimes the request gets cancelled and even if its successful (200) i do not get send the token in the response.
When i make a postman request the request is always successful and sends the token, also when i make the request in Angular constructor without the refresh logic, so i suspect it has something to do with the usage of rxjs but can not figure out whats the problem.
here is the logic of refresh in app.component
constructor(
private router: Router,
private auth: AuthService
) {
// this.auth.requestServer(ServerMethod.GET, '/refresh').subscribe() // this request would work fine
router.events.pipe(
switchMap((event) => {
if (event instanceof NavigationStart) {
const browserRefresh = !router.navigated;
if (browserRefresh) {
this.auth.deleteAccessToken();
return this.auth.requestServer(ServerMethod.GET, '/refresh');
}
}
return EMPTY;
})
).subscribe();
}
here is deleteAccessToken()
deleteAccessToken() {
sessionStorage.removeItem('accessToken');
this.tokenSubject.next(null);
}
requestServer()
requestServer(type: ServerMethod, path?: string, data?: any): Observable<any> {
let httpOptions: httpOptions;
switch (type) {
case ServerMethod.POST:
return this.server.post(path, data).pipe(tap(res => this.handleAccessToken(res)));
case ServerMethod.GETALL:
return this.server.getAll(this.getAllPath);
case ServerMethod.GET:
return this.server.get(path).pipe(tap(res => this.handleAccessToken(res)));
default:
return EMPTY;
}
}
here is server get method
get(path: string): Observable<any> {
const url = this.serverAdress + path;
return this.http.get(url);
}
and in my nodejs backend here is the refresh logic:
module.exports.refresh_get = async (req, res) => {
if (req.cookies && req.cookies.refreshToken) {
// Destructuring refreshToken from cookie
const refreshToken = req.cookies.refreshToken;
// Verifying refresh token
jwt.verify(refreshToken, 'secret',
(err, decodedToken) => {
if (err) {
// Wrong Refesh Token
res.status(406).json({ message: 'wrong refresh token' });
}
else {
// create new accessToken
const accessToken = createToken(decodedToken.id);
// create new refreshToken and set it in cookie and delete old cookie
const newRefreshToken = jwt.sign({
id: decodedToken.id,
}, 'secret', { expiresIn: '1d' });
res.cookie('refreshToken', newRefreshToken, { httpOnly: true,
sameSite: 'None', secure: true,
maxAge: 24 * 60 * 60 * 1000 });
res.status(200).json({ accessToken });
}
})
} else {
res.status(406).json({ message: 'Unauthorized' });
}
}
request in network tab on refresh looks then like this:
but Response is empty, there should be an object { accessToken: '...' }

ChatGPT answered my question:
It's possible that the problem lies with the switchMap operator in the router.events observable. The switchMap operator cancels the previous inner observable when a new value is emitted, which could result in the HTTP request being cancelled if it takes too long to complete.
To ensure that the HTTP request is not cancelled, you can try using the concatMap operator instead of switchMap. concatMap will wait for the previous observable to complete before starting a new one, which will prevent the HTTP request from being cancelled prematurely.
Thanks ChatGPT.

Related

"Sign in with Apple" redirects to a blank white page on PWA

I've implemented "Sign in with Apple" on my site. When I try it on my phone, it redirects me to a blank white page with the same URL as the redirect_uri I've configured.
I can't find any info on why this is happening. What's a possible fix?
UPDATE
It seems as if Apple JS SDK is creating a FORM HTML DOM element, sets the POST URL of the FORM to point to the redirect_uri, and finally programmatically clicks form.submit(). This for some reason causes the page to navigate to the redirect_uri and show the POST response as a new page.
I figured this out by tracking the Apple JS SDK in the debugger.
Here is my code
//---- Frontend ----
AppleID.auth.init({
clientId : '<client_id>',
scope : 'email',
redirectURI : 'mySite.com/apple_auth',
state : 'origin:web',
nonce : Date.now(),
//usePopup : true //not using this one. When false or undefined, Apple will make a POST request to the defined redirect_uri
})
// Listen for authorization success.
document.addEventListener('AppleIDSignInOnSuccess', (event) => {
// Handle successful response.
console.log(event.detail.data);
});
// Listen for authorization failures.
document.addEventListener('AppleIDSignInOnFailure', (event) => {
// Handle error.
console.log(event.detail.error);
});
//...
myButton.onClick = ()=>{
try {
var res = await AppleID.auth.signIn()
} catch(err) {
var x = 0
}
}
//---- Backend ----
var appleSignin = require("apple-signin-auth")
app.express.post('/apple_auth', async (req, res)=>{
var body = req.body
try {
const appleRes = await appleSignin.verifyIdToken(
body.id_token, // We need to pass the token that we wish to decode.
{
audience: '<client_id', // client id - The same one we used on the frontend, this is the secret key used for encoding and decoding the token.
ignoreExpiration: true, // Token will not expire unless you manually do so.
}
)
//do something with the Apple response
} catch (err) {
// Token is not verified
console.error(err)
}
})
From the documentation...
The HTTP body contains the result parameters with a content-type of application/x-www-form-urlencoded.
Make sure you've configured the urlencoded() body-parsing middleware in your Express app.
app.use(express.urlencoded());
Make sure you check for errors and actually send a response from your /apple_auth Express route
const { code, id_token, state, user, error } = req.body;
if (error) {
return res.status(500).send(error);
}
try {
const appleRes = await appleSignin.verifyIdToken(id_token, {
audience: "<client_id>",
ignoreExpiration: true,
});
// do something with the Apple response, then send a response
res.send(appleRes.sub);
} catch (err) {
console.error(err);
res.sendStatus(500); // send a 500 response status
}

Sending a cookie as a response with Firebase Callable Functions

I am trying to send a cookie with options set to it as a response using a Firebase callable cloud function (https.onCall). I see in the Firebase docs that this can be done with express:
(The below is taken directly form the Firebase docs)
app.post('/sessionLogin', (req, res) => {
// Get the ID token passed and the CSRF token.
const idToken = req.body.idToken.toString();
const csrfToken = req.body.csrfToken.toString();
// Guard against CSRF attacks.
if (csrfToken !== req.cookies.csrfToken) {
res.status(401).send('UNAUTHORIZED REQUEST!');
return;
}
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// To only allow session cookie setting on recent sign-in, auth_time in ID token
// can be checked to ensure user was recently signed in before creating a session cookie.
getAuth()
.createSessionCookie(idToken, { expiresIn })
.then(
(sessionCookie) => {
// Set cookie policy for session cookie.
const options = { maxAge: expiresIn, httpOnly: true, secure: true };
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({ status: 'success' }));
},
(error) => {
res.status(401).send('UNAUTHORIZED REQUEST!');
}
);
});
I have implemented the callable function, but I do now know how to attach the options to my cookie string.
The below is my code:
// I want the return type to be a Promise of a cookie object, not a string
export const setCookie = https.onCall(async (context: https.CallableContext): Promise<string> => {
try {
console.log(context);
const auth: Auth = getAuth();
const idToken: DecodedIdToken = await auth.verifyIdToken(context.instanceIdToken!); // https://firebase.google.com/docs/auth/admin/verify-id-tokens#web
console.log("idToken: ", idToken);
const cookie: string = await auth.createSessionCookie(idToken.uid, { expiresIn: 300000 });
const options = {
maxAge: 300000,
httpOnly: true,
secure: true,
sameSite: "strict",
};
// res.cookie("session", cookie, options);
return cookie; // should be assigned to __session cookie with domain .web.app
// httpOnly=true, secure=true and sameSite=strict set.
} catch (error) {
console.log("ERROR FOUND: ", error);
throw new https.HttpsError("unknown", "Error found in setCookie");
}
});
Is there any way I can do this using a Callable Firebase Cloud Function? All the documentation and resources I have found require express to send an cookie with Node.
Thanks!
The documentation you're linking to assumes you are writing standard nodejs backend code using express. However, your code is using a callable type function. They are not the same and do not have the same capabilities. Callable functions don't let you set cookies in the response. You can only send a JSON payload back to the client; the SDK handles all of the HTTP headers and they are outside of your control.
Perhaps you should look into using a standard HTTP type function (onRequest), where you do have some control over the headers in the response.

How to clear an set cookies with Apollo Server

I recently just switched from using express with apollo server to just using apollo server since the subscriptions setup seemed more current and easier to setup. The problem I'm having now is I was saving a cookie with our refresh token for login and clearing the cookie on logout. This worked when I was using express.
const token = context.req.cookies[process.env.REFRESH_TOKEN_NAME!];
context.res.status(401);
Since switching from express/apollo to just apollo server I don't have access to req.cookies even when i expose the req/res context on apollo server.
I ended up switching to this (which is hacky) to get the cookie.
const header = context.req.headers.cookie
var cookies = header.split(/[;] */).reduce(function(result: any, pairStr: any) {
var arr = pairStr.split('=');
if (arr.length === 2) { result[arr[0]] = arr[1]; }
return result;
}, {});
This works but now I can't figure out how to delete the cookies. With express I was doing
context.res.clearCookie(process.env.REFRESH_TOKEN_NAME!);
Not sure how I can clear cookies now since res.clearCookie doesn't exist.
You do not have to specifically clear the cookies. The expiresIn cookie key does that for you. Here is the snippet which i used to set cookies in browser from apollo-server-lambda. Once the expiresIn date values has passed the current date time then the cookies wont be valid for that host/domain. You need to revoke access token for the user again or logout the user from the application
import { ApolloServer, AuthenticationError } from "apollo-server-lambda";
import resolvers from "./src/graphql/resolvers";
import typeDefs from "./src/graphql/types";
const { initConnection } = require("./src/database/connection");
const { validateAccessToken, hasPublicEndpoint } = require("./src/bll/user-adapter");
const { addHoursToDate } = require("./src/helpers/utility");
const corsConfig = {
cors: {
origin: "http://localhost:3001",
credentials: true,
allowedHeaders: [
"Content-Type",
"Authorization"
],
},
};
// creating the server
const server = new ApolloServer({
// passing types and resolvers to the server
typeDefs,
resolvers,
context: async ({ event, context, express }) => {
const cookies = event.headers.Cookie;
const accessToken = ("; " + cookies).split(`; accessToken=`).pop().split(";")[0];
const accessLevel = ("; " + cookies).split(`; accessLevel=`).pop().split(";")[0];
const expiresIn = ("; " + cookies).split(`; expiresIn=`).pop().split(";")[0];
const { req, res } = express;
const operationName = JSON.parse(event.body).operationName;
if (await hasPublicEndpoint(operationName)) {
console.info(operationName, " Is a public endpoint");
} else {
if (accessToken) {
const jwtToken = accessToken.split(" ")[1];
try {
const verifiedUser = await validateAccessToken(jwtToken);
console.log("verifiedUser", verifiedUser);
if (verifiedUser) {
return {
userId: verifiedUser,
};
} else {
console.log();
throw new AuthenticationError("Your token does not verify!");
}
} catch (err) {
console.log("error", err);
throw new AuthenticationError("Your token does not verify!");
}
}
}
return {
headers: event.headers,
functionName: context.functionName,
event,
context,
res,
};
},
cors: corsConfig,
formatResponse: (response, requestContext) => {
if (response.data?.authenticateUser || response.data?.revokeAccessToken) {
// console.log(requestContext.context);
const { access_token, user_type, access_token_generated_on, email } =
response.data.authenticateUser || response.data.revokeAccessToken;
const expiresIn = addHoursToDate(new Date(access_token_generated_on), 12);
requestContext.context.res.set("set-cookie", [
`accessToken=Bearer ${access_token}`,
`accessLevel=${user_type}`,
`expiresIn=${new Date(access_token_generated_on)}`,
`erUser=${email}`,
]);
}
if (response.data?.logoutUser) {
console.log("Logging out user");
}
return response;
},
});
Simply send back the exact same cookie to the client with an Expires attribute set to some date in the past. Note that everything about the rest of the cookie has to be exactly the same, so be sure to keep all the original cookie attributes, too.
And, here's a link to the RFC itself on this topic:
Finally, to remove a cookie, the server returns a Set-Cookie header
with an expiration date in the past. The server will be successful
in removing the cookie only if the Path and the Domain attribute in
the Set-Cookie header match the values used when the cookie was
created.
As to how to do this, if you're using Node's http module, you can just use something like this (assuming you have a response coming from the callback passed to http.createServer):
context.response.writeHead(200, {'Set-Cookie': '<Your Cookie Here>', 'Content-Type': 'text/plain'});
This is assuming that your context has access to that http response it can write to.
For the record, you can see how Express does it here and here for clarity.

i want to get my jwt value from cookies in browser

i have now stored my jwt in cookies when user sign in or sign up but the data don't stay so i made a function to handle this but i need the value of the token to make it work
this is the function that i need token value for
const setAuthToken = (token) => {
if (token) {
axios.defaults.headers.common['x-auth-token'] = token;
} else {
delete axios.defaults.headers.common['x-auth-token'];
}
};
and this is my action that i use in react to send the token value to this function i tried to use js-cookies for that but it give me undefined
import Cookies from 'js-cookie';
//load user
export const loadUser = () => async (dispatch) => {
const token = Cookies.get('access_token');
console.log(token);
// if (cookie.access_token) {
// setAuthToken(cookie.access_token);
// }
try {
const res = await axios.get('/user/me');
dispatch({
type: USER_LOADED,
payload: res.data,
});
} catch (err) {
dispatch({
type: AUTH_ERROR,
});
}
};
and this is my recieved cookie in browser
If you take a close look at your screenshot, you can see that the cookie is sent by the server as HttpOnly. This is a security measure, and therefore the cookie isn't accessible to any JavaScript code by design.
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Restrict_access_to_cookies
If you are in control of the server, you could change it accordingly, if not you will have to make a deal :-)
res.cookie('x-auth-token',token,{
maxAge: 3600,
httpOnly: true,
secure:true
})

How to implement auto refresh token in graphql for jwt based authentication?

I am trying to figure out this scenario for my JWT based authentication in Apollo based graphql server (2.0) .
Basically after login a user gets accessToken and refreshToken from server.
AccessToken gets expired after certain period of time and server sends an error message indicating that token expired (TokenExpiredError) and then client need to communicate with server for new accessToken via passing refreshToken.
Flow is as following -
TokenExpiredError occurs
Get that error on client side
Queue all requests with old accessToken(so that server is not flooded with too many refreshToken calls and many accessTokens are generated by server)
Call refreshToken api on graphql server to get new accessToken
update accessToken for all authorised calls with new accessToken
Logout user incase refreshToken itself is expired
Prevent any kind of race condition b/w calls
I have already implemented refreshToken mutation on client side but can't figure out about when error occurs stop all requests -> request new token -> make all pending request again and if refresh token is expired logout user.
I followed this approach to solve my problem finally
Posting my approach for others
// #flow
import { ApolloLink, Observable } from 'apollo-link';
import type { ApolloClient } from 'apollo-client';
import type { Operation, NextLink } from 'apollo-link';
import { refreshToken2, getToken } from './token-service';
import { GraphQLError } from 'graphql';
export class AuthLink extends ApolloLink {
tokenRefreshingPromise: Promise<boolean> | null;
injectClient = (client: ApolloClient): void => {
this.client = client;
};
refreshToken = (): Promise<boolean> => {
//if (!this.tokenRefreshingPromise) this.tokenRefreshingPromise = refreshToken(this.client);
if (!this.tokenRefreshingPromise) this.tokenRefreshingPromise = refreshToken2();
return this.tokenRefreshingPromise;
};
setTokenHeader = (operation: Operation): void => {
const token = getToken();
if (token) operation.setContext({ headers: { authorization: `Bearer ${token}` } });
};
request(operation: Operation, forward: NextLink) {
// set token in header
this.setTokenHeader(operation);
// try refreshing token once if it has expired
return new Observable(observer => {
let subscription, innerSubscription, inner2Subscription;
try {
subscription = forward(operation).subscribe({
next: result => {
if (result.errors) {
console.log("---->", JSON.stringify(result.errors))
for (let err of result.errors) {
switch (err.extensions.code) {
case 'E140':
console.log('E140', result)
observer.error(result.errors)
break;
case 'G130':
this.refreshToken().then(response => {
if (response.data && !response.errors) {
this.setTokenHeader(operation);
innerSubscription = forward(operation).subscribe(observer);
} else {
console.log("After refresh token", JSON.stringify(response));
observer.next(response)
}
}).catch(console.log);
break;
}
}
}
observer.next(result)
},
complete: observer.complete.bind(observer),
error: netowrkError => {
observer.error(netowrkError);
}
},
});
} catch (e) {
observer.error(e);
}
return () => {
if (subscription) subscription.unsubscribe();
if (innerSubscription) innerSubscription.unsubscribe();
if (inner2Subscription) inner2Subscription.unsubscribe();
};
});
}
}

Resources