I want to login and set a localStorage token on the client (specifically jwt)
How can I accomplish this using cy.request, as suggested in the Cypress Documentation?
Here's an example of adding a command cy.login() that you can use in any Cypress test, or put in a beforeEach hook.
Cypress.Commands.add('login', () => {
cy.request({
method: 'POST',
url: 'http://localhost:3000/api/users/login',
body: {
user: {
email: 'jake#jake.jake',
password: 'jakejake',
}
}
})
.then((resp) => {
window.localStorage.setItem('jwt', resp.body.user.token)
})
})
Then in your test:
beforeEach(() => {
cy.login()
})
As an extra, you can also use the cypress-localstorage-commands package to persist localStorage between tests, so login request will be made only once:
In support/commands.js:
import "cypress-localstorage-commands";
Cypress.Commands.add('login', () => {
cy.request({
method: 'POST',
url: 'http://localhost:3000/api/users/login',
body: {
user: {
email: 'jake#jake.jake',
password: 'jakejake',
}
}
})
.its('body')
.then(body => {
cy.setLocalStorage("jwt", body.user.token);
})
});
Then, in your tests:
before(() => {
cy.login();
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
I've used something along the lines of bkuceras answer for a while now. Recently I've run into an issue when testing multiple roles/logins throughout tests. Basically what is happening is I log in as an admin and do one test, then I log in as a non admin and do a second test. The first test runs fine and ends (cypress clears local storage) however there are still some xhr requests running from the first test when the second test starts. These xhr requests no longer see the token which triggers my app to see a 401 unauthorized error and clear local storage (including my non admin token I have set). Now the 2nd test fails because that non admin token is not available in local storage.
Ultimately my solution was to prefetch the token before the test then set the token in the onBeforeLoad function of visit. This ensures the token is not cleared by any race condition before your visit command.
cy.visit('/app', {
onBeforeLoad: function (window) {
window.localStorage.setItem('token', myToken);
}
})
Really this is a pretty unique edge case, but heres hoping it may help someone as I've spent many hours finding this solution.
If you are open for the experimental mode, I highly recommend to use the cy.session to store the token:
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.request({
method: 'POST',
url: '/login',
body: { username, password },
}).then(({ body }) => {
window.localStorage.setItem('authToken', body.token)
})
})
})
For more information, please check the official cypress documentation:
https://docs.cypress.io/api/commands/session
I think that title should be updated for this topic. JWT token is main potin for this discussion!
The main question was about JWT token but in general, all modern applications are using OIDC Microsoft article - v2-protocols-oidc / ADAL and here is a very tricky situation to get an access using just generating tokens by API call.
I found here that
and will check it with my app. But be sure that you are cool in JS and have good assistance from your DevTeam ;)
I have spent so many hours on this and finally I can safely conclude that it will never work for OAuth requests.
It may work for local server but not when you getting token for authentication.
Related
What I want to achieve
A user, who logged in or signed up should not re-login after one hour. The restriction of one hour comes from firebase authentication, if not prevented (what I try to accomplish).
Problem
After a user is logged in via firebase authentication (signInWithEmailAndPassword) I always get null for currentUser and onAuthStateChanged.
What I tried
I'm using React (v17.0.2) using 'Create React App'. On server side I'm using NodeJS (v12). The communication between both is accomplished using axios (v0.21.1)
First I tried to send the token stored in localStorage, which came from firebase (server side), back to the server. But the server tells me, that the token is no longer valid. Server side code as follows:
module.exports = (request, response, next) => {
let idToken;
if (request.headers.authorization && request.headers.authorization.startsWith('Bearer ')) {
idToken = request.headers.authorization.split('Bearer ')[1];
console.log("idToken:", idToken);
} else {
console.error('No token found');
return response.status(403).json({ error: 'Unauthorized' });
}
admin
.auth()
.verifyIdToken(idToken)
.then((decodedToken) => {
console.log('decodedToken', decodedToken);
request.user = decodedToken;
return db.collection('users').where('userId', '==', request.user.uid).limit(1).get();
})
.catch((err) => {
console.error('Error while verifying token', err);
return response.status(403).json(err);
});
};
After that I tried the following code on client side.
handleSubmit = () => {
const userData = {
email: this.state.email,
password: this.state.password
};
axios
.post(firestoreUrl() + '/login', userData)
.then((resp) => {
console.log("token:", resp.data); //here I get a valid token
localStorage.setItem('AuthToken', `Bearer ${resp.data.token}`);
console.log("firebase.auth().currentUser:", firebase.auth().currentUser); //but this is null
})
.then((data) => {
console.log(data);
console.log("firebase.auth().currentUser:", firebase.auth().currentUser); //still null
})
.catch((error) => {
console.log("Error:", error);
});
};
What irritates me is that I get a token from firebase (server side), the token is then stored in localStorage (client side) but firebase then tells me, that the currentUser is null. But presumably they are not mutually dependent =/.
I'm able to access all secured sites in my app. I can log out and in again. But whatever I do the currentUser is null.
I also tried to run the code above in componentDidMount()-method. But no success.
I tried an approach from this link (hopefully in a way it should be), but it didn't work. Still getting null for both currentUser and onAuthStateChanged if I implement following code.
firebase.auth().onAuthStateChanged(user => {
if (user) {
console.log("state = definitely signed in")
}
else {
console.log("state = definitely signed out")
}
})
I always get logged to the console, that the user is 'definitely signed out'.
During research I noticed that the point at which I should try to get the currentUser-Status is kind of tricky. So I guess that one solution is to implement the currentUser-code at another/the right place. And here I'm struggling =/.
As I found out at a similar question here on SO, I did a bad mistake. Apparently, it's not a good idea to perform the signIn- or createUser-functionality on server side. This should be done on client side. In the question mentioned above are some good reasons for doing that on server side but in my case it's quite ok to run it on client side.
Thanks to Frank van Puffelen for leading the way (see one of the comments in the question mentioned above).
In the past, I have used firebase.auth in the web client and once a user creates another user, I link certain security logic:
Once the user has been created I send an email to verify your email
with the function user.sendEmailVerification ().
As the user was created by another user, I assign a default password
and use the sendPasswordResetEmail () function so that the user
registers his new password.
That has worked well for me so far, but now for many reasons I need to move that logic to my server, for that I'm developing a backend with cloud functions and I'm using the Node.js Firebase Admin SDK version 6.4.0, but I can not find a way to use the functions of user.sendEmailVerification() and sendPasswordResetEmail() to implement the same logic on the server, the closest thing I found was:
auth.generateEmailVerificationLink (email)
auth.generatePasswordResetLink (email)
But it only generates a link for each one, which by the way the only emailVerification() serves me, the one from generatePasswordReset always tells me:
Try resetting your password again
Your request to reset your password has expired or the link has
already been used.
Even though be a new link, and it has not been used.
My 3 questions would be:
How can I make the sendEmailVerification () and
sendPasswordResetEmail () functions work on the server?
How can I make the link generated with
auth.generatePasswordResetLink (email) work correctly on the server?
Is there any way to use templates and emails on the server that are
in firebase auth?
Thank you in advance for sharing your experience with me, with all the programmers' community of stack overflow.
Those functions are not available in firebase-admin, but you should be able to run the client-side SDK (firebase) on the server as well. Not exactly a best practice, but it will get the job done. There's a long standing open feature request to support this functionality in the Admin SDK. You will find some helpful tips and workarounds there.
Could be a bug. I would consider reporting it along with a complete and minimal repro. The Admin SDK does have an integration test case for this use case, but it works slightly differently.
Not at the moment. Hopefully, this will be covered when the above feature request is eventually fulfilled.
The is a workaround provided here
https://github.com/firebase/firebase-admin-node/issues/46
I found a work-around that works well enough for my use case, see below. I'm not sure if this is best practice, but I wanted to keep the emails exactly the same between the server and client requests. Would love to hear about any flaws with this implementation 💡
As suggested above, it uses a three step process to do this:
Acquire a custom token via the admin sdk's createCustomToken(uid)
It converts this custom token to an idToken via the API
It invokes the send email verification endpoint on the API
const functions = require('firebase-functions');
const fetch = require('node-fetch');
const admin = require('firebase-admin');
const apikey = functions.config().project.apikey;
const exchangeCustomTokenEndpoint = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apikey}`;
const sendEmailVerificationEndpoint = `https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=${apikey}`;
module.exports = functions.auth.user().onCreate(async (user) => {
if (!user.emailVerified) {
try {
const customToken = await admin.auth().createCustomToken(user.uid);
const { idToken } = await fetch(exchangeCustomTokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: customToken,
returnSecureToken: true,
}),
}).then((res) => res.json());
const response = await fetch(sendEmailVerificationEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestType: 'VERIFY_EMAIL',
idToken: idToken,
}),
}).then((res) => res.json());
// eslint-disable-next-line no-console
console.log(`Sent email verification to ${response.email}`);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
});
I'm sure it doesn't matter anymore, but I had a headache doing this so I'd like to share even if it isn't the greatest answer.
await admin.auth().createUser(
{email, password, displayName, phoneNumber, photoURL}
).then(function(userRecord) {
admin.auth().createCustomToken(userRecord.uid).then(function(customToken){
createdToken=customToken;
firebase.auth().signInWithCustomToken(createdToken).catch(function(error){
return console.log(error)
})
firebase.auth().onAuthStateChanged(function(user) {
user.sendEmailVerification().then(function(){
return console.log('It worked')
},function(error) {
return console.log(error)
})
});
})
})
I'm currently writing a program in which multiple accounts need to be authenticated through the LinkedIn REST API. This needs to be done in fairly quick succession as it's quite common to have to re-connect multiple accounts at once due to LinkedIn's fairly short-lived tokens.
The issue is that after authenticating a single account I then have to wait ~10 minutes for the session to expire before authenticating a new user. When the API is called (liLogin) it will automatically log the user in as part of the existing session.
I believe this is an intentional feature of the API as you can see mentioned in Step 5 of The Authorization Code Flow Documentation.
Other things to note. I've tried adding a random string as a query on the callback URL which was mentioned in another thread. I've also noticed the Android SDK has a logout function but can't seem to locate anything for this within the REST API.
I've pasted the part of my code that handles this below in-case this helps in anyway.
// Login
const liLogin = async (req, res) => {
const URL = `https://www.linkedin.com/oauth/v2/authorization?client_id=${config.li.id}&redirect_uri=${config.li.redirectUri}&response_type=code&state=PostSchedulerCapture321&scope=r_liteprofile%20r_emailaddress%20w_member_social`;
res.redirect(URL);
};
// Exchange temp token for access_token
const liGetUser = (req, res) => {
let token = req.query.code;
axios.get(`https://www.linkedin.com/oauth/v2/accessToken`, {
params: {
"grant_type": "authorization_code",
"code": token,
"redirect_uri": config.li.redirectUri,
"client_id": config.li.id,
"client_secret": config.li.secret
}
})
.then(result => {
return result.data.access_token;
})
.then(access_token => {
axios.get('https://api.linkedin.com/v2/me', {
'headers': {
'Authorization': `Bearer ${access_token}`
}
})
.then(result => {
res.send(result.data)
})
.catch(err => {
console.log(err);
})
})
.catch(err => {
err = {...err.config.headers, ...err.response.data};
console.log(err);
})
}
In-case anyone comes across this you can simply redirect the browser to the logout URL (https://linkedin.com/m/logout) and then use setTimeout to close the window after a few seconds.
Not an ideal solution but it works.
I have a piece of middleware that performs authentication with a third-party service via request. I do this request with superagent. Obviously I want to mock this in my tests since it slows them down quite a lot and also is dependant on the third-parties server.
When using nock, it doesn't seem to find the request at all. I even tried using the recorder and it only picks up the actual requests of my local endpoints. (Although it uses an unfamiliar IP and port?).
The request inside my middleware;
export default async (req, res, next) => {
const user = await superagent
.get(`https://example.com/session/`)
.query({ session })
.set('Api-Key', '1234');
}
My Nock Instance;
nock('https://example.com/session/')
.persist()
.get('/session/')
.reply(200, {
success: true,
username: 'testuser',
})
.log(console.log);
You have 2 issues here.
First off, you define twice /session/:
nock('https://example.com/session/')
.get('/session/')
Choose:
nock('https://example.com').get('/session/')
nock('https://example.com/session/').get('/')
Second issue, you're adding a query string to your call (.query({ session })), but you don't tell that to Nock using .query(true).
In the end, you should have something like:
nock('https://example.com/session/')
.persist()
.get('/') // rewrote here
.query(true) // added here
.reply(200, {
success: true,
username: 'testuser',
})
.log(console.log);
I'm currently developing a node express postgresql application, and I'm trying to implement Jsonwebtokens as authentication. I've seen multiple tutorials on how to implement it and I get how to do it on the backend part, but the frontend is usually skipped and apparently everyone just tests their code with Postman.
I have also read online that the recommended way to implement jwt authentication is to store the generated token in localstorage, and, when needed, to send it on the header. But I wasn't able to find how this is done...
Thus, my questions are:
How do you store the token on the front-end once it's generated by the backend? (an example would help a lot, because I don't really get how am I supposed to get the token on a front-end javascript program)
How do you send the token on the headers when making an http request that needs it once you have it stored?
On the server side, once you have created the token and logged the user in, you send the token via res.send(), example below, note that you may have different approach to functions findByCredentials ad genereateAuthToken, they are custom:
app.post("/users/login", async (req, res) => {
try {
const user = await User.findByCredentials(
req.body.email,
req.body.password
);
const token = await user.generateAuthToken();
res.send({ token: user.tasks });
} catch (e) {
res.status(400).send();
}
});
On the frontend you can use html5's fetch() to send the token in the header. For example, if you would like to access '/users/me' that needs authentication you follow the steps below (make sure you however you save the token to localstorage first so you can access that via getItem:
localStorage.setItem('userInfo', JSON.stringify(userInfo));
document.getElementById("my-profile").addEventListener("click", getMe);
then:
function getMe(e) {
e.preventDefault();
var token = JSON.parse(localStorage.getItem('token'));
console.log(`Authorization=Bearer ${token}`)
fetch('/users/me', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(res => res.json())
.then(data => {
console.log(data)
// window.location.href = 'http://localhost:3000/dashboard';
})
.catch(err => { console.log(err) })
}
As you said, usually the token is store in localStorage.
localStorage is similar to sessionStorage, except that while data
stored in localStorage has no expiration time, data stored in
sessionStorage gets cleared when the page session ends — that is, when
the page is closed.
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
For getting the token in front-end you send to a URL the email & password of the user in order to exchange it with a token (you have to be in https). After that you store it with localStorage.setItem('key', value)
Short example:
$.post("/authenticate", {email: userEmail, password: userPassword}, function(data) {
localStorage.setItem('token', data.token)
});
For get back the token, after a refresh for example, you have to use : localStorage.getItem('key').
And finally, in order to be authenticate with this token, you can send it in bearer headers in Authorization headers property.
Why bearer ? => https://security.stackexchange.com/questions/108662/why-is-bearer-required-before-the-token-in-authorization-header-in-a-http-re
Example:
$.ajax({
type: 'GET',
url: '/account,
headers: {
"Authorization": "Bearer " + token
}
}, function(data) {
// Authenticated data
});
May this can help : https://github.com/auth0-blog/angularjs-jwt-authentication-tutorial/blob/master/frontend/login/login.js