I am trying to implement a login mechanism using Firebase custom token and session cookies, for sure I am doing something wrong but I cannot figure it out.
I will put my code and then explain how I am using it to test it.
Frontend code
const functions = firebase.functions();
const auth = firebase.auth();
auth.setPersistence(firebase.auth.Auth.Persistence.NONE);
auth.onAuthStateChanged((user) => {
if (user) {
user.getIdToken()
.then((idToken) => {
console.log(idToken);
});
}
});
function testCustomLogin(token) {
firebase.auth().signInWithCustomToken(token)
.then((signInToken) => {
console.log("Login OK");
console.log("signInToken", signInToken);
signInToken.user.getIdToken()
.then((usertoken) => {
let data = {
token: usertoken
};
fetch("/logincookiesession", {
method: "POST",
body: JSON.stringify(data)
}).then((res) => {
console.log("Request complete! response:", res);
console.log("firebase signout");
auth.signOut()
.then(()=> {
console.log("redirecting ....");
window.location.assign('/');
return;
})
.catch(() => {
console.log("error during firebase.signOut");
});
});
});
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
console.log(errorCode, errorMessage);
});
}
Backend code
app.post('/logincookiesession', (req, res) => {
let token = req.body.token;
// 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.
admin.auth().createSessionCookie(token, {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'}));
})
.catch((error) => {
res.status(401).send('UNAUTHORIZED REQUEST!' + JSON.stringify(error));
});
});
app.get('/logintest', (req, res) => {
let userId = 'jcm#email.com';
let additionalClaims = {
premiumAccount: true
};
admin.auth().createCustomToken(userId, additionalClaims)
.then(function(customToken) {
res.send(customToken);
})
.catch(function(error) {
console.log('Error creating custom token:', error);
});
});
so basically what I do is
execute firebase emulators:start
manually execute this on my browser http://localhost:5000/logintest , this gives me a token printed in the browser
Then in another page, where I have the login form, I open the javascript console of the browser and I execute my javascript function testCustomLogin and I pass as a parameter the token from step 2.
in the network traffic I see that the call to /logincookiesession return this:
UNAUTHORIZED REQUEST!{"code":"auth/invalid-id-token","message":"The provided ID token is not a valid Firebase ID token."}
I am totally lost.
I can see in the firebase console, in the Authentication section that user jcm#email.com is created and signed-in, but I cannot create the session-cookie.
Please,I need some advice here.
The route for creating the cookie session had an error.
It should start like this.
app.post('/logincookiesession', (req, res) => {
let params = JSON.parse(req.body);
let token = params.token;
And the code I used was from a manual, OMG. I hope this helps someone too.
Related
Node.js CODE
exports.user = async (req, res) => {
try {
const { wallet } = req.body;
if (!wallet) {
res.status(400).json({ error: "Not logged in" });
return;
} else {
user = User.findone(wallet);
// if user is not found then create a new user and mark as loggged In
if (!user) {
User.create({
user: wallet,
});
}
// if user found then create a session token and mark as logged
in
res.send({
user: wallet,
});
}
} catch (error) {
console.log(`ERROR::`, error);
}
};
REACTJs CODE
// post call/update
const axiosCall = async () => {
// core login will give a unique username by fulling a transcation
// core.login i dont have any control
const userAccount = await core.login();
try {
const res = await Axios.post(`${API}/user`, userAccount, dataToken);
setData({
...data,
error: "",
success: res.data.message,
});
} catch (error) {
setData({
...data,
error: error.response.data.error,
});
}
};
Now here the problem occurs when some one could modify userAccount in the front-end or someone could send a body with wallet: anything to my route localhost:3000/api/user
There is no option for me to check if some actually used core.login(); to get the wallet address.
So is there any solution?
I was thinking to allow only my server IP or localhost to hit the route localhost:3000/api/user and is that even possible?
Also there is another issue anyone could modify userAccount in front-end.
I am trying to understand how I can present two different views for each type of user that I have via Node js. I have 2 different types of users, 'restaurants' and 'customers'. Both users need to login in. However, the profile page of each type of user is a little different. Currently, both 'restaurants' and 'customers' are in different collections. I'd like to redirect the restaurant users to 'restaurant.hbs' and the customers to 'customer.hbs' with their corresponding information from the database.
Here is the Firebase functionality which enables the login of a user for either customer or restaurant owner.
firebase.auth().signInWithEmailAndPassword(email, password).then(({user}) => {
// Get the user's ID token as it is needed to exchange for a session cookie.
console.log(user)
return user.getIdToken().then(idToken => {
// Session login endpoint is queried and the session cookie is set.
// CSRF protection should be taken into account.
// ...
console.log(idToken)
return fetch("/sessionLogin", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"CSRF-Token": Cookies.get("XSRF-TOKEN"),
},
body: JSON.stringify({ idToken }),
});
});
}).then(() => {
// A page redirect would suffice as the persistence is set to NONE.
return firebase.auth().signOut();
}).then(() => {
window.location.assign('/profile');
});
Here is what I have written for the page redirect for restaurants
function renderRestaurantPage(restaurantId, res) {
getRestaurant(restaurantId).then(data => {
let ambassadorPromise = getAmbassadorInfo(restaurantId)
let activityPromise = getActivityFeed(restaurantId)
var restaurantName = data['name']
var totalScans = data['total_scans']
Promise.all([ambassadorPromise, activityPromise]).then(values => {
let ambassadorInfo = values[0]
let activityInfo = values[1]
let ambassadorList = []
let activityList = []
ambassadorInfo.forEach(element => {
ambassadorList.push(element.data())
})
activityInfo.forEach(element => {
activityList.push(element.data())
});
res.render('index', {restaurantName, totalScans, ambassadorList, activityList})
})
})
}
app.get("/profile", function (req, res) {
console.log("We are about to look at session cookie")
const sessionCookie = req.cookies.session || "";
console.log("Printing session cookie")
console.log(sessionCookie)
admin
.auth()
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
.then((decodedClaims) => {
console.log(decodedClaims.user_id)
renderRestaurantPage(decodedClaims.user_id, res)
})
.catch((error) => {
console.log(error)
});
});
app.post('/sessionLogin', (req, res) => {
console.log("in session login")
console.log(req.body)
const idToken = req.body.idToken;
// 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.
admin
.auth()
.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' }));
console.log("cookie created")
},
(error) => {
res.status(401).send('UNAUTHORIZED REQUEST!');
}
);
});
You should be managing the users 'type' from the users custom claims, this allows you to assign a user type as 'user' vs 'restaurant' that is accessible within your app and within your database.
For example, if the user is a restaurant, you would show buttons that allow the user to access a restaurant management panel, etc. The same would be done within Security Rules, where if the user's token.restaurant == true or token.type == "restaurant"
Custom Claims can only be edited from the admin-sdk, and you can read about them from here and how to use them within Security Rules here.
I am using the JWT token to verify my API requests. Access token expires in 1 minute, and refresh token expires in 1 year. After the access token expires, an API request is sent with a refresh token to get a new set of tokens. A new set of tokens are only sent if the refresh token is valid, and exists in the database. I am using Axios interceptor to achieve this. Everytyhing seems to work fine for some time. However, it logs me out even when the refresh token is valid and does exist in DB. I am assuming I am missing something in Axios interceptor or has to do with async functions.
Error logs "code does not match" in server-side at verifyRefreshToken function, and "Error here" in client-side updateToken function.
CLIENT SIDE CODE
API.js
// Response interceptor for API calls
API.interceptors.response.use((response) => {
return response
}, async (error) => {
// reject promise if network error
if (!error.response) {
console.log("Network Error");
return Promise.reject(error);
}
const originalRequest = error.config;
console.log(store.getState().auth)
// if access token is expired
if (error.response.status === 403 && error.response.data.message == "token expired") {
// var refreshToken = await getRefreshToken() // get refresh token from local storage
var refreshToken = await store.getState().auth.refreshToken
// restore tokens using refresh token
await store.dispatch(await updateToken(refreshToken)) // get new set of tokens from server and store tokens in redux state
// var newAccessToken = await getToken() // get token from local storage
var newAccessToken = await store.getState().auth.accessToken
if(newAccessToken != null){
originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`;
return API(originalRequest)
}
return Promise.reject(error);
}
// if refresh token is expired or does not match
if (error.response.status === 403 && error.response.data.message == "false token") {
socketDisconnect() // disconnect socket connection
signOut() // remove tokens from local storage
store.dispatch(logOut()) // set tokens in redux as null
return Promise.reject(error);
}
return Promise.reject(error);
});
updateTokenFunction
export const updateToken = (rt) => {
return async (dispatch) => {
const data = await API.post('/auth/refreshToken', {
token: rt
})
.then(async res => {
var accessToken = res.data.accessToken
var refreshToken = res.data.refreshToken
await storeToken(accessToken) // store access token in local storage
await storeRefreshToken(refreshToken) // store refresh token in local storage
dispatch(restoreToken({accessToken, refreshToken})) // store token in redux state
})
.catch(err => {
console.log("err here" + err) // LOG SHOWS ERROR HERE
})
}
}
SERVER SIDE CODE
// /auth/refreshToken
// POST: /api/auth/refreshToken
router.post('/', (req, res) => {
var { token } = req.body
if(!token) res.status(403).send({"status":false, "message": "false token", "result": ""})
verifyRefreshToken(token)
.then(async data => {
var userName = data.userName
// get new tokens
var accessToken = await getAccessToken(userName)
var refreshToken = await getRefreshToken(userName)
res.json({"status":true, "message": "token verified", "accessToken": accessToken, "refreshToken": refreshToken})
})
.catch(err => {
console.log(err);
res.status(403).send({"status":false, "message": "false token", "result": ""})
})
});
To generate new refresh token
// generate refresh token
const getRefreshToken = (userName) => {
return new Promise((resolve, reject) => {
var secret = process.env.REFRESH_TOKEN_SECRET
var options = { expiresIn: '1y' }
jwt.sign({userName},secret , options, (err, token) => {
if(err) reject("error")
var data = {"userName": userName, "token": token}
// delete all expired token from database
dbQueries.deleteRefreshToken(data, result => {
})
// add refresh token to database
dbQueries.addRefreshToken(data, result => {
if(result == "success"){
console.log("added token " + token);
resolve(token)
}else{
reject("failure")
}
})
});
})
}
Verifying refresh token
// verify access token
const verifyRefreshToken = (token) => {
return new Promise((resolve, reject) => {
var secret = process.env.REFRESH_TOKEN_SECRET
if(!token) return reject("no token")
jwt.verify(token, secret, (err, user) => {
if(err){
return reject(err)
}
// check if the verified token and token from database matches
var data = {"userName": user.userName}
dbQueries.getRefreshToken(data, result => {
if(result.length == 0){
return reject("no data")
}
if(token === result[0].token){
resolve(user)
} else{
reject("code does not match") // LOGS THIS ERROR
}
})
})
})
}
UPDATE
The error was due to multiple API calls at the same time, and all had requested for a new access token, with old refresh tokens. I solved the issue using code in this link.
I'm using AWS for my website. After 1 hour the token expires and the user pretty much can't do anything.
For now i'm trying to refresh the credentials like this:
function getTokens(session) {
return {
accessToken: session.getAccessToken().getJwtToken(),
idToken: session.getIdToken().getJwtToken(),
refreshToken: session.getRefreshToken().getToken()
};
};
function getCognitoIdentityCredentials(tokens) {
const loginInfo = {};
loginInfo[`cognito-idp.eu-central-1.amazonaws.com/eu-central-1_XXX`] = tokens.idToken;
const params = {
IdentityPoolId: AWSConfiguration.IdPoolId
Logins: loginInfo
};
return new AWS.CognitoIdentityCredentials(params);
};
if(AWS.config.credentials.needsRefresh()) {
clearInterval(messwerte_updaten);
cognitoUser.refreshSession(cognitoUser.signInUserSession.refreshToken, (err, session) => {
if (err) {
console.log(err);
}
else {
var tokens = getTokens(session);
AWS.config.credentials = getCognitoIdentityCredentials(tokens);
AWS.config.credentials.get(function (err) {
if (err) {
console.log(err);
}
else {
callLambda();
}
});
}
});
}
the thing is, after 1hour, the login token gets refreshed without a problem, but after 2hrs i can't refresh the login token anymore.
i also tried using AWS.config.credentials.get(), AWS.config.credentials.getCredentials() and AWS.config.credentials.refresh()
which doesn't work either.
The error messages i'm getting are:
Missing credentials in config
Invalid login token. Token expired: 1446742058 >= 1446727732
After almost 2 weeks i finally solved it.
You need the Refresh Token to receive a new Id Token. Once the Refreshed Token is acquired, update the AWS.config.credentials object with the new Id Token.
here is an example on how to set this up, runs smoothly!
refresh_token = session.getRefreshToken(); // you'll get session from calling cognitoUser.getSession()
if (AWS.config.credentials.needsRefresh()) {
cognitoUser.refreshSession(refresh_token, (err, session) => {
if(err) {
console.log(err);
}
else {
AWS.config.credentials.params.Logins['cognito-idp.<YOUR-REGION>.amazonaws.com/<YOUR_USER_POOL_ID>'] = session.getIdToken().getJwtToken();
AWS.config.credentials.refresh((err)=> {
if(err) {
console.log(err);
}
else{
console.log("TOKEN SUCCESSFULLY UPDATED");
}
});
}
});
}
Usually it's solved by intercepting http requests with additional logic.
function authenticationExpiryInterceptor() {
// check if token expired, if yes refresh
}
function authenticationHeadersInterceptor() {
// include headers, or no
}}
then with use of HttpService layer
return HttpService.get(url, params, opts) {
return authenticationExpiryInterceptor(...)
.then((...) => authenticationHeadersInterceptor(...))
.then((...) => makeRequest(...))
}
It could be solved by proxy as well http://2ality.com/2015/10/intercepting-method-calls.html
In relation to AWS:
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html
You're interested in:
getPromise()
refreshPromise()
Here is how I implemented this:
First you need to authorize the user to the service and grant permissions:
Sample request:
Here is how I implemented this:
First you need to authorize the user to the service and grant permissions:
Sample request:
POST https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/token&
Content-Type='application/x-www-form-urlencoded'&
Authorization=Basic aSdxd892iujendek328uedj
grant_type=authorization_code&
client_id={your client_id}
code=AUTHORIZATION_CODE&
redirect_uri={your rediect uri}
This will return a Json something like:
HTTP/1.1 200 OK
Content-Type: application/json
{"access_token":"eyJz9sdfsdfsdfsd", "refresh_token":"dn43ud8uj32nk2je","id_token":"dmcxd329ujdmkemkd349r", "token_type":"Bearer", "expires_in":3600}
Now you need to get an access token depending on your scope:
POST https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/token
Content-Type='application/x-www-form-urlencoded'&
Authorization=Basic aSdxd892iujendek328uedj
grant_type=client_credentials&
scope={resourceServerIdentifier1}/{scope1} {resourceServerIdentifier2}/{scope2}
Json would be:
HTTP/1.1 200 OK
Content-Type: application/json
{"access_token":"eyJz9sdfsdfsdfsd", "token_type":"Bearer", "expires_in":3600}
Now this access_token is only valid for 3600 secs, after which you need to exchange this to get a new access token. To do this,
To get new access token from refresh Token:
POST https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/token >
Content-Type='application/x-www-form-urlencoded'
Authorization=Basic aSdxd892iujendek328uedj
grant_type=refresh_token&
client_id={client_id}
refresh_token=REFRESH_TOKEN
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{"access_token":"eyJz9sdfsdfsdfsd", "refresh_token":"dn43ud8uj32nk2je", "id_token":"dmcxd329ujdmkemkd349r","token_type":"Bearer", "expires_in":3600}
You get the picture right.
If you need more details go here.
This is how you can refresh access token using AWS Amplify library:
import Amplify, { Auth } from "aws-amplify";
Amplify.configure({
Auth: {
userPoolId: <USER_POOL_ID>,
userPoolWebClientId: <USER_POOL_WEB_CLIENT_ID>
}
});
try {
const currentUser = await Auth.currentAuthenticatedUser();
const currentSession = currentUser.signInUserSession;
currentUser.refreshSession(currentSession.refreshToken, (err, session) => {
// do something with the new session
});
} catch (e) {
// whatever
}
};
More discussion here: https://github.com/aws-amplify/amplify-js/issues/2560.
I'm trying to use the Spotify wrapper to use my app, I manage to hit Spotify's Account Service and the page to authorize the app to use a users spotify account information but I get an internal 500 server error:
index.js:6 POST https://accounts.spotify.com/en/authorize/accept 500 (Internal Server Error)
// Set up Spotify API wrapper
const scopes = ['user-read-private', 'user-read-email'];
const STATE_KEY = 'spotify_auth_state';
app.get('/login', (req, res) => {
const state = generateRandomString(16);
res.cookie(STATE_KEY, state);
res.redirect(spotifyApi.createAuthorizeURL(scopes, state));
});
app.get('/callback', (req, res) => {
const { code, state } = req.query;
const storedState = req.cookies ? req.cookies[STATE_KEY] : null;
if (state === null || state !== storedState) {
res.redirect('/#/error/state mismatch');
} else {
res.clearCookie(STATE_KEY);
spotifyApi
.authorizationCodeGrant(code)
.then(data => {
const { expires_in, access_token, refresh_token } = data.body;
// Set the access token on the API object to use it in later calls
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
spotifyApi.getMe().then(({ body }) => {
console.log(body);
});
res.redirect('/search');
})
.catch(err => {
res.redirect('/#/error/invalid token');
});
}
});
This is my endpoint on that page:
https://accounts.spotify.com/en/authorize?client_id=CLIENT_ID&response_type=code&redirect_uri=http:%2F%2Flocalhost:8888%2Fcallback%2F&scope=user-read-private%20user-read-email&state=k4ogwe4h53i00000
Not entirely sure what's happening. I thought it was initially a state mismatch but the state generated is fine. Having trouble pinpointing where the error is. Anyone have any suggestions?
You need to set your redirect URI in the developer dashboard, ensuring that it's exactly the same as the one you're using in your app.
We're aware of the poor error handling for the invalid redirect URI error and it's being fixed.