I am trying to figure out if following is secure and correct way of handling user authentication with websockets when using socket.io. My understanding is that we only need to verify authentication (jwt token) during socket connection and can assume that all subsequent events are authenticated?
I essentially made this middleware to verify jwt token once during connect event and then set user details on socket.
socketIo.use((socket, next) => {
socket.on('connect', async () => {
try {
const { authToken } = socket.handshake.auth
const user = await verifyUserToken(authToken)
socket.user = {
uid: user.uid
}
next()
} catch {
next(new Error(SocketError.UNAUTHORIZED))
}
})
})
Is this correct and secure, or do I need to verify token on every event call?
Related
I'm new to web sockets (specifically to socketIO interfaces, both client and server), and I wonder how basic JWT auth is usually implemented. How the headers are passed through. Should I add them to every socket listener or emit event? Or just once during the connection. Maybe the connection is already considered private because of the connection id?
I am using Socket.io-client v.4 and flask-socketio.
I am interested in general practices and would be grateful for any information or your own experience.
There are many ways to pass authentication when you connect:
HTTP header
Cookie
query string
auth option
To pass a token in a header (not valid when connecting directly via WebSocket):
const socket = io({
extraHeaders: {
"Header-Name": "abcd"
}
});
Same site cookies are always passed to the server. To pass cookies cross-site:
const socket = io("https://my-backend.com", {
withCredentials: true
});
To pass it in the query string:
const socket = io({
query: {
token: 'abcd'
}
});
To pass it with the auth option (Socket.IO v3 and up only):
const socket = io({
auth: {
token: "abcd"
}
});
I am in a next-js app and my auth token is stored in cookies.
For some raisons i use Swr and Api route to fetch my secured api backend.
i am trying to find a way to put my auth token in all api request.
During login cookie is set
res.setHeader(
'Set-Cookie',
cookie.serialize('token', data.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
maxAge: data.expires_in, // 1 week
sameSite: 'strict',
path: '/',
}),
);
This is an example of a page using swr fetch
//page/test.ts - example of my test route
const { data, error } = useFetchContent(id);
if (error) {
showError('error');
replace('/');
}
return <DisplayContent content={data} />
This is a swrFetchHook
// fetchContentHook
function useFetchContent(id: string): ContentDetail {
return useSWR<any>(`/api/content/${id}`, fetcherApiRoute);
}
const fetcherApiRoute = (url: string): Promise<any> => {
return axios(url)
.then((r) => r.data)
.catch((err) => {
console.info('error is ', err)
throw err
});
};
export default useFetchContent;
inside api route
export default async (req, res): Promise<ContentDetail> => {
const { id } = req.query;
if (req.method === 'GET') {
const fetchRealApi = await apiAxios(url);
if(fetchRealApi) {
// here depending on result of fetchRealApi i add some other fetch ...
return res.status(200).json({ ...fetchRealApi, complement: comp1 });
}
return res.status(500)
}
return res.status(500).json({ message: 'Unsupported method only GET is allowed' });
};
and finally api axios configuration
const apiAxios = axios.create({
baseURL: '/myBase',
});
apiAxios.interceptors.request.use(
async (req) => {
// HERE i am trying to get token from cookies
// and also HERE if token is expired i am trying to refresh token
config.headers.Authorization = token;
req.headers['Content-type'] = 'application/x-www-form-urlencoded';
return req;
},
(error) => {
return Promise.reject(error);
},
);
export default apiAxios;
I am stuck here because i cant find token during apiAxios.interceptors.request.use...
Did you know what i am doing wrong, and am i on a correct way to handle this behavior ?
To allow sending server cookie to every subsequent request, you need to set withCredentials to true. here is the code.
const apiAxios = axios.create({
baseURL: '/myBase',
withCredentials: true,
});
Nilesh's answer is right if your API is able to authorize requests based on cookies. Also it needs the API to be in the same domain as your frontend app. If you need to send tokens to the API (the one which is in the cookie), then you will need a small backend component often called BFF or Token Handler. It can extract the token from the cookie and put in an Authorization header.
At Curity we've created a sample implementation of such a Token Handler, of which you can inspire: https://github.com/curityio/kong-bff-plugin/ You can also have a look at an overview article of the Token Handler pattern.
I am learning MongoDb Realm from the documentation and following the examples given. My code to login is
const loginEmailPassword = async (email, password) => {
const app = Realm.App.getApp(APP_ID)
const credentials = Realm.Credentials.emailPassword(email, password)
try {
// Authenticate the user
const user = await app.logIn(credentials)
return true
} catch (err) {
console.error('Failed to log in', err)
}
return false
}
If the credentials match a current user all is good, if the don't it doesn't throw an error but generates the following in the console:
POST https://eu-west-1.aws.stitch.mongodb.com/api/client/v2.0/app/<"my Realm App ID">/auth/providers/local-userpass/login 401
Failed to log in Error: Request failed (POST https://eu-west-1.aws.stitch.mongodb.com/api/client/v2.0/app/<"my Realm App ID">/auth/providers/local-userpass/login): invalid username/password (status 401)
Can anyone please help me understand what is going on?
I am writing some end-to-end test cases to test socket connections in my app. I expect receiving socket events after specific rest API requests. For instance, after hitting: /api/v1/[createTag], I expect receiving createTag event to be captured by socket.io-client. The issue is that, it is very inconsistently passing, and sometimes failing, with good rest API requests. The reason to fail is that done() event inside socket.on('createTag' ... is never called, so it gets timeout. On browser, currently all the API endpoints and sockets seem to be working fine. Is there a specific configuration that I might be missing in order to test socket.io-client within Node.js environment and Jest?
Below is my test cases, and thanks a lot in advance:
describe('Socket integration tests: ', () => {
beforeAll(async done => {
await apiInit();
const result = await requests.userSignIn(TEST_MAIL, TEST_PASSWORD);
TEST_USER = result.user;
SESSION = result.user.session;
console.log('Test user authenticated succesfully.');
done();
});
beforeEach(done => {
socket = io(config.socket_host, { forceNew: true })
socket.on('connect', () => {
console.log('Socket connection succesful.');
socket.emit('session', { data: SESSION }, (r) => {
console.log('Socket session successful.');
done();
});
});
})
test('Receiving createTag socket event?', async(done) => {
console.log('API request on createTag');
const response = await Requester.post(...);
console.log('API response on createTag', response);
socket.on('createTag', result => {
console.log('createTag socket event succesful.');
createdTagXid = result.data.xid;
done();
})
});
afterEach(done => {
if(socket.connected) {
console.log('disconnecting.');
socket.disconnect();
} else {
console.log('no connection to break');
}
done();
})
}
Basically, setting event handles after async API calls seems to be the issue. So I should have first set the socket.on( ... and then call rest API.
I have logged into parse from cloud code using Parse.User.logIn
After a successful login have retrieved session token using user._session
Parse.User.logIn(request.params.userdata.email, "sdfisadufhkasdjhf", {
success: function(user) {
response.success(user._sessionToken);
},
error: function(user, error) {
}
});
This session token is passed to the client which then makes a REST API call by setting the token in the header.
However, the rest API call is not successful and returns invalid session token error.
REST API call works perfect when I don't send session token for requests that don't need authentication
From chrome console, I can see that the headers are set correctly and the value of session token is same as Parse.User.current().getSessionToken()
in app.config()
$httpProvider.defaults.headers.post['X-Parse-Application-Id'] = "dxfhgfxhxhxhxxxxxxxxxxxxxxxxxxxxxdgerstrattgrft";
$httpProvider.defaults.headers.post['X-Parse-REST-API-Key'] = "gfhjjhfjfjjchfjcccccccccccccccccccccccccccccccccccc";
$httpProvider.defaults.headers.post['Content-Type'] = "application/json";
From controller
$scope.createGroup = function()
{
shan = $scope.creategroup;
$http.post('https://api.parse.com/1/functions/addGroup', $scope.creategroup,
{ headers: {
'X-Parse-Session-Token':sessionToken
}
}).success(function(data, status, headers, config) {
alert("success : "+JSON.stringify(data));
}).
error(function(data, status, headers, config) {
alert("error : "+JSON.stringify(data));
});
}