I have the following code in my firebase front-end:
document.querySelector('#sign-out').addEventListener('click', () => {
firebase.auth().signOut().then(() => {
document.location = '/sign-in';
}, (error) => {
console.error('Sign Out Error', error);
});
})
This seems to be working. I see the network request succeed, and the redirect to /sign-in occurs.
However, when I then navigate to /chatroom, I am considered logged in. My python/flask backend has the following code on the endpoint:
#app.route("/chatroom")
def chatroom():
if 'idToken' in request.cookies:
id_token = request.cookies.get("idToken")
try:
decoded_token = auth.verify_id_token(id_token)
except:
print("INVALID TOKEN")
return redirect(url_for("sign_in"))
return render_template("chatroom.html")
else:
return redirect(url_for("sign_in"))
Rather than redirecting the user to /sign-in when I hit this path, Firebase is verifying that the token is valid and so I am allowed to proceed, even though I had logged out.
What am I missing?
Edit:
Changed code to this for debugging purposes:
firebase.auth().signOut().then(() => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log('still signed in');
} else {
console.log('signed out');
}
});
}, (error) => {
console.error('Sign Out Error', error);
});
It outputs signed out.....so I'm quite confused.
Signing out does not invalidate the token. It just causes the SDK to forget about it, so it doesn't get refreshed or passed along to other Firebase products automatically. The old token will be valid for up to 1 hour, until it needs to be refreshed. If not refreshed, then it will fail verification.
If you are saving the ID token in a cookie, you should also remove it from the cookie in order to effectively sign out, so it doesn't get passed along to your backend.
Related
(I've read a number of similar questions here, and most/all have said to use a different axios instance for the refresh token requests (versus the API requests). However, I'm not clear on how that would work, since I am using axios-auth-refresh for auto-refreshing the access tokens.)
I'm working on an app with a JWT-based authentication flow for back-end API requests. The general flow is working fine; upon login the user gets a long-term refresh token and short-term access token. Using the axios-auth-refresh plug-in for axios, I am able to auto-refresh the access token when it has expired.
My problem is, when the refresh token expires, I am not able to catch the error and redirect the user to re-authenticate. Nothing I've tried catches the error. The (current) code for the auto-refresh hook is:
const refreshAuth = (failed) =>
axios({ method: "post", url: "token", skipAuthRefresh: true })
.then(({ status, data: { success, accessToken } }) => {
console.warn(`status=${status}`);
if (!success) Promise.reject(failed);
processToken(accessToken);
// eslint-disable-next-line no-param-reassign
failed.response.config.headers.Authorization = `Bearer ${accessToken}`;
return Promise.resolve();
})
.catch((error) => console.error("%o", error));
createAuthRefreshInterceptor(axios, refreshAuth);
In cases of the refresh token being stale or missing, I see neither the status=xxx console line nor the dump of an error object in the catch() block.
The actual file this is in is on GitHub here, though it is slightly different than the working version above. Mainly, in the GH version the hook calls axios.post("token").then(...) where above I'm making a more explicit call to add the skipAuthRefresh parameter. Adding that got me more detailed error traces in the console, but I am still not catching the 401 response via the catch().
I've tried everything I can think of... anything jump out as something I'm missing?
Randy
(Edited to ensure the GitHub link points to the version of the file that has the issue.)
Since posting this, I have managed to work through the problem and come up with a working solution.
The key to the solution does in fact lie in using a different axios instance for the calls to renew the refresh token. I created a second module to encapsulate a second axios instance that would not get the interceptor created by the axios-auth-refresh module. After working around some inadvertent circular-dependency issues that this initially caused, I reached a point where I could see the exception being thrown by axios when the refresh token itself is stale or missing.
(Interestingly, this led to another problem: once I recognized that the refresh token was no longer valid, I needed to log the user out and have them return to the login screen. Because the application this is in is a React application, the authentication was being handled with custom hooks, which can only be called within a component. However, I had abstracted all the API calls into a non-React module so that I could encapsulate things like the addition of the Authorization header, the base URL, etc. At that level I could not run the auth hook to get access to the logout logic. I solved this by putting a default onError handler on the query object (a react-query object) that I use for all the API calls.)
I built upon the Request class from this SO answer to refresh the token and handle the refresh failures.
Now my Request looks like this:
import axios from "axios";
import {getLocalStorageToken, logOut, refreshToken} from "./authentication";
class Request {
ADD_AUTH_CONFIG_HEADER = 'addAuth'
constructor() {
this.baseURL = process.env.REACT_APP_USER_ROUTE;
this.isRefreshing = false;
this.failedRequests = [];
this.axios = axios.create({
baseURL: process.env.REACT_APP_USER_ROUTE,
headers: {
clientSecret: this.clientSecret,
},
});
this.beforeRequest = this.beforeRequest.bind(this);
this.onRequestFailure = this.onRequestFailure.bind(this);
this.processQueue = this.processQueue.bind(this);
this.axios.interceptors.request.use(this.beforeRequest);//<- Intercepting request to add token
this.axios.interceptors.response.use(this.onRequestSuccess,
this.onRequestFailure);// <- Intercepting 401 failures
}
beforeRequest(request) {
if (request.headers[this.ADD_AUTH_CONFIG_HEADER] === true) {
delete request.headers[this.ADD_AUTH_CONFIG_HEADER];
const token = getLocalStorageToken();//<- replace getLocalStorageToken with your own way to retrieve your current token
request.headers.Authorization = `Bearer ${token}`;
}
return request;
}
onRequestSuccess(response) {
return response.data;
}
async onRequestFailure(err) {
console.error('Request failed', err)
const {response} = err;
const originalRequest = err.config;
if (response.status === 401 && err && originalRequest && !originalRequest.__isRetryRequest) {
if (this.isRefreshing) {
try {
const token = await new Promise((resolve, reject) => {//<- Queuing new request while token is refreshing and waiting until they get resolved
this.failedRequests.push({resolve, reject});
});
originalRequest.headers.Authorization = `Bearer ${token}`;
return this.axios(originalRequest);
} catch (e) {
return e;
}
}
this.isRefreshing = true;
originalRequest.__isRetryRequest = true;
console.log('Retrying request')
console.log('Previous token', getLocalStorageToken())
try {
const newToken = await refreshToken()//<- replace refreshToken with your own method to get a new token (async)
console.log('New token', newToken)
originalRequest.headers.Authorization = `Bearer ${newToken}`;
this.isRefreshing = false;
this.processQueue(null, newToken);
return this.axios(originalRequest)
} catch (err) {
console.error('Error refreshing the token, logging out', err);
await logOut();//<- your logout function (clean token)
this.processQueue(err, null);
throw response;//<- return the response to check on component layer whether response.status === 401 and push history to log in screen
}
}
throw response;
}
processQueue(error, token = null) {
this.failedRequests.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
this.failedRequests = [];
}
}
const request = new Request();
export default request;
My problem is, when the refresh token expires, I am not able to catch
the error and redirect the user to re-authenticate. Nothing I've tried
catches the error. The (current) code for the auto-refresh hook is:
What is the return code from your api if the access token expired ?
if it is different than 401 (default) you need to configure, see exanoke 403:
createAuthRefreshInterceptor(axios, refreshAuthLogic, {
statusCodes: [ 401, 403 ] // default: [ 401 ]
});
i am using graph api javascript example from here https://learn.microsoft.com/en-us/graph/api/user-list-joinedteams?view=graph-rest-beta&tabs=javascript
and my code is like:
async function(req, res) {
if (!req.isAuthenticated()) {
// Redirect unauthenticated requests to home page
res.redirect('/')
} else {
let params = {
active: { calendar: true }
};
// Get the access token
var accessToken;
try {
accessToken = await tokens.getAccessToken(req);
console.log("access token is:", accessToken)
} catch (err) {
req.flash('error_msg', {
message: 'Could not get access token. Try signing out and signing in again.',
debug: JSON.stringify(err)
});
}
if (accessToken && accessToken.length > 0) {
try {
console.log("vik testing stuff12 for teams")
const user = await graph.getTeams(accessToken)
console.log("graph me:::", user)
} catch (err) {
req.flash('error_msg', {
message: 'Could not fetch events',
debug: JSON.stringify(err)
});
}
} else {
req.flash('error_msg', 'Could not get an access token');
}
res.render('calendar', params);
}
}
getTeams is
getTeams: async function(accessToken) {
const client = getAuthenticatedClient(accessToken);
const events = await client
.api('/me/joinedTeams')
.version('beta')
.get();
return events;
}
this prints no results and no error. if I replace 'me/joinedTeams' to just 'me' then it returns logged in user details.
You can got a response successfully, so it seems no error with your code as you said if you call https://graph.microsoft.com/v1.0/me you can get user information.
And I tried to call this API using my account(my account hasn't joined any Teams), and got response like below, so if you got the same response as mine, perhaps you need to check if you have joined any Teams:
On the other hand, following the document, this API needs several permissions. So please obtain your access token when debug and use JWT tool to decrypt it to check if the access token have enough scope.
And I used the same request and got Teams information after adding my account to a team.
I have made a full stack application with a register activity successfully adding to db.
How would I conditionally render the home page dependent on if the login is correct.
In my login route I have an if statement which successfully logs "bad creds" if do not exist or "login: login successful.." if it does.
I added a redirect into the handle submit(this is triggered once the login form button is pressed) which was supposed to be triggered if successful (it technically is but it determines "bad creds successful as well").
I have attempted an if stametn but I am not sure how to use this with express middle ware.
the logic I would want the the portion of handle submit to do is something along the lines of
if (login successful){
window.location.href = "/home";
}
else {
window.location.href = "/login";
(preferably with a alert )
}
Login route
app.post("/login", async (req, response) => {
try {
await sql.connect(config);
var request = new sql.Request();
var Email = req.body.email;
var Password = req.body.password;
console.log({ Email, Password });
request.input("Email", sql.VarChar, Email);
request.input("Password", sql.VarChar, Password);
var queryString =
"SELECT * FROM TestLogin WHERE email = #Email AND password = #Password";
//"SELECT * FROM RegisteredUsers WHERE email = #Email AND Password = HASHBYTES('SHA2_512', #Password + 'skrrt')";
const result = await request.query(queryString);
if (result.recordsets[0].length > 0) {
console.info("/login: login successful..");
console.log(req.body);
req.session.loggedin = true;
req.session.email = Email;
response.send("User logined");
} else {
console.info("/login: bad creds");
response.status(400).send("Incorrect email and/or Password!");
}
} catch (err) {
console.log("Err: ", err);
response.status(500).send("Check api console.log for the error");
}
});
handleSubmit(e) {
e.preventDefault();
if (this.state.email.length < 8 || this.state.password.length < 8) {
alert(`please enter the form correctly `);
} else {
const data = { email: this.state.email, password: this.state.password };
fetch("/login", {
method: "POST", // or 'PUT'
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json"
},
body: JSON.stringify(data)
})
// .then(response => response.json())
.then(data => {
console.log("Success:", data);
// if ( ) {
// console.log("nice");
// } else {
// console.log("not nice");
// }
// window.location.href = "/home";
})
.catch(error => {
console.error("Error:", error);
});
}
}
catch(e) {
console.log(e);
}
You should have explained in the first place that you had a React app in the frontend. Talking at the same time about Express middleware and login route is a bit messy. :)
What you're doing is a login/sign in process through an API. This means your server should return JSON information regarding the login outcome. Then, your frontend should handle that in whatever way you want to. This means that your server should simply treat the login request as any other data request. Return a status code and some optional JSON data.
Authentication is a BIG subject and since you did't provide many details, I can only tell you how normally the overall process should go:
Send the user credentials to the server (like you do in your POST request)
Handle the response received from the server. If login was successful, you should receive some information from the server, like the user id, email, session id, either in the response JSON data or by HTTP headers. You should keep this information in the frontend, normally in localStorage, and use it for every request to the server to provide your identity. You should look up JSON Web Tokens.
In your React app, you want to check when starting the application if the user is already logged in or not (using the piece of information mentioned in step 2, or trying to fetch and endpoint that returns user information like /me). If you don't have that information or the request fails, redirect to Login.
In your React app, in your login page, handle the fetch result and redirect to home if the user is authenticated, or stay there and display whatever info you want.
I assume that since you're using user login some resources should be protected from being accessed by non logged in users or restricted depending on the logged in user. This is done with middleware on your Express server, that should check the user id / token / session id information your React app should be sending with every request.
To redirect using React Router, you don't want to use window.location. You want to use the Router itself to avoid reloading the full page. You can either use the injected history prop on your Login route component or wrap any component that needs it with withRouter HOC.
This article seem to lay out all options using React Router pretty well:
https://serverless-stack.com/chapters/redirect-on-login-and-logout.html
Hope this helps, this is a complex subject that you should split into smaller problems and tackle one at a time. ;)
Currently, when the user goes through the Social auth (via redirects), it successfully creates a user under Firebase Authentication, but fails to retrieve the Google/Facebook API's data. Perplexed as to how it's failing to retrieve the data when the Auth is successful.
I used to only have the SignInWithPopup (which works perfectly fine), but am trying to get getRedirectResults to work too (for mobile).
Given the behavior I've been seeing, the problem is most likely with the getRedirectResults. Most likely not the social API setup, because otherwise the user would never get created in Authentication.
The following code resides in componentDidMount (this is a React.js app):
if (this.state.device === 'mobile') {
firebase.auth().getRedirectResult().then(function(result) {
console.log('login successful! Data is:');
console.log(result);
if (result.credential) {
var provider = result.credential.providerId;
self.handleSocialAuth(provider, result);
}
}).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
});
}
results is supposed to contain the user's data, keeps returning:
login successful! Data is:
{user: null}
I have not deviated from official docs examples, which is why I'm confused.
firebase.auth().getRedirectResult() - is called after the page loads.
Official doc link: https://firebase.google.com/docs/auth/web/google-signin
Use the below method to retrieve the user that is logged in.
firebase.auth().onAuthStateChanged(function(user) {
console.log(user);
});
If no user is logged in the user will be null.
Live example:
checkAuthStatus(){
firebase.auth().onAuthStateChanged(user => {
this.setState({ btnWithImg: user });
if(user !== null){
this.setState({ userIsLoggedIn: true });
this.props.toggleLogIn();
}
});
}
You should call firebase.auth().getRedirectResult() only when user has been authenticated. Example
firebase.auth().onAuthStateChanged(user => {
if (user) {
const result = await firebase.auth().getRedirectResult();
assert(result.user, 'user is empty')
}
});
I am having trouble using the Parse Server JS SDK to edit and save a user.
I am signing in, logging in and retrieving the user just fine, I can call without exception user.set and add/edit any field I want, but when I try to save, even when using the masterKey, I get Error 206: Can t modify user <id>.
I also have tried to use save to direcly set the fields, same result.
A interesting thing is that in the DB, the User's Schema get updated with the new fields and types.
Here is my update function:
function login(user, callback) {
let username = user.email,
password = user.password;
Parse.User.logIn(username, password).then(
(user) => {
if(!user) {
callback('No user found');
} else {
callback(null, user);
}
},
(error) => {
callback(error.message, null);
}
);
}
function update(user, callback) {
login(user, (error, user) => {
if(error) {
callback('Can t find user');
} else {
console.log('save');
console.log('Session token: ' + user.getSessionToken());
console.log('Master key: ' + Parse.masterKey);
user.set('user', 'set');
user.save({key: 'test'}, {useMasterKey: true}).then(
(test) => {
console.log('OK - ' + test);
callback();
}, (err) => {
console.log('ERR - ' + require('util').inspect(err));
callback(error.message);
}
);
}
});
}
And a exemple of the error:
update
save
Session token: r:c29b35a48d144f146838638f6cbed091
Master key: <my master key>
ERR- ParseError { code: 206, message: 'cannot modify user NPubttVAYv' }
How can I save correctly my edited user?
I had the exact same problem when using Parse Server with migrated data from an existing app.
The app was created before March 2015 when the new Enhanced Sessions was introduced. The app was still using legacy session tokens and the migration to the new revocable sessions system was never made. Parse Server requires revocable sessions tokens and will fail when encountering legacy session tokens.
In the app settings panel, the Require revocable sessions setting was not enabled before the migration and users sessions were not migrated to the new system when switching to Parse Server. The result when trying to edit a user was a 400 Bad Request with the message cannot modify user xxxxx (Code: 206).
To fix the issue, I followed the Session Migration Tutorial provided by Parse which explain how to upgrade from legacy session tokens to revocable sessions. Multiple methods are described depending on your needs like enableRevocableSession() to enable these sessions on a mobile app, if you're only having a web app, you can enforce that any API requests with a legacy session token to return an invalid session token error, etc.
You should also check if you're handling invalid session token error correctly during the migration to prompt the user to login again and therefore obtain a new session token.
I had the same error and neither useMasterKey nor sessionToken worked for me either. :(
Here's my code:
console.log("### attempt 1 sessionToken: " + request.user.getSessionToken());
var p1 = plan.save();
var p2 = request.user.save(null, {sessionToken: request.user.getSessionToken()});
return Parse.Promise.when([p1, p2]).then(function(savedPlan) {
...
}
I see the matching session token in log output:
2016-08-21T00:19:03.318662+00:00 app[web.1]: ### attempt 1 sessionToken: r:506deaeecf8a0299c9a4678ccac47126
my user object has the correct ACL values:
"ACL":{"*":{"read":true},"PC7AuAVDLY":{"read":true,"write":true}}
I also see a bunch of beforeSave and afterSave logs with user being "undefined". not sure whether that's related.
beforeSave triggered for _User for user undefined:
I'm running latest parser-server version 2.2.18 on Heroku (tried it on AWS and results are the same)
function login(logInfo, callback) {
let username = logInfo.email,
password = logInfo.password;
Parse.User.logIn(username, password).then(
(user) => {
if(!user) {
callback('No user found');
} else {
callback(null, user);
}
},
(error) => {
callback(error.message, null);
}
);
}
function update(userInfo, data, callback) {
login(userInfo, (error, user) => {
if(error) {
callback('Can t find user');
} else {
getUpdatedData(user.get('data'), data, (error, updateData) => {
if(error) {
callback(error);
} else {
user.save({data: updateData}, /*{useMasterKey: true}*/ {sessionToken: user.get("sessionToken")}).then(
(test) => {
callback();
}, (err) => {
callback(error.message);
}
);
}
});
}
});
}
For some reason, retrying to use sessionToken worked.
This is not how asynchronous functions work in JavaScript. When createUser returns, the user has not yet been created. Calling user.save kicks off the save process, but it isn't finished until the success or error callback has been executed. You should have createUser take another callback as an argument, and call it from the user.save success callback.
Also, you can't create a user with save. You need to use Parse.User.signUp.
The function returns long before success or error is called.