Firebase admin sdk: idToken OR custom token verification for authorization - javascript

I'm creating a custom token on backend server while login and below is the code:
UserSchema.methods.generateAuthToken = function() {
var user = this;
var access = "auth";
return firebaseAdmin.default
.auth()
.createCustomToken(user._id.toHexString())
.then(function(token) {
user.tokens = user.tokens.concat([{ access, token }]);
return user.save().then(() => {
return token;
});
});
};
and storing this token inside mongodb and sending it back to client via header named "x-auth" and then stored it inside cookies.
On client side, using this token to signin like below:
axios({
url: "/api/users/login",
method: "POST",
data: {
email: data.email,
password: data.password
}
}).then(res => {
Cookies.set("x-auth", res.headers["x-auth"], { expires: 7 });
return firebase
.auth()
.signInWithCustomToken(res.headers["x-auth"])
.then(response => {
console.log("CA", response);
response
.getIdToken()
.then(idToken => {
Cookies.set("idToken", idToken, { expires: 7 });
})
.catch(e => {});
dispatch(
login({
token: Cookies.get("x-auth")
})
);
});
});
Now in order to call an api to fetch all users, I'm sending these tokens, custom token and idToken back to server:
const authenticate = (req, res, next) => {
const token = req.header("x-auth");
const idToken = req.header("idToken");
User.findByToken(token, idToken)
.then(user => {
if (!user) {
return Promise.reject({
message: "no user found with the associated token!"
});
}
req.user = user;
req.token = token;
next();
})
.catch(e => {
res.status(401).send(setErrorMessage(e.message));
});
};
And findByToken is as below:
UserSchema.statics.findByToken = function(token, idToken) {
var User = this;
return firebaseAdmin.default
.auth()
.verifyIdToken(idToken)
.then(function(decodedToken) {
var uid = decodedToken.uid;
return User.findOne({
_id: uid,
"tokens.access": "auth",
"tokens.token": token
});
})
.catch(function(e) {
return Promise.reject(e);
});
};
Why do I have to send both these tokens for authorization, is there anything wrong with the concept here.
https://firebase.google.com/docs/auth/admin/verify-id-tokens
Warning: The ID token verification methods included in the Firebase Admin SDKs are meant to verify ID tokens that come from the client SDKs, not the custom tokens that you create with the Admin SDKs. See Auth tokens for more information.
Please clarify if I can verify the custom token instead of idToken to retrieve userId and match it up with DB, instead of using different tokens for one purpose or I'm doing something wrong here and custom token should not be stored inside DB, and there is some other approach to it.
And now after sometime when I try to fetch all users, it says:
Firebase ID token has expired. Get a fresh token from your client app and try again (auth/id-token-expired). See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.

When working with tokens, you need to differentiate each token and what it is used for, also each token has related properties.
Custom Tokens: These tokens will be generated in the server (as you are doing) and are used in the client side to authenticate the client. These tokens expire after one hour.
Session ID Token: (I'm calling it "Session ID Token" just to differentiate it) When the client autheticates with any of the authentication providers, the SDK will exchange the information for an ID token used for the session. The same applies for custom tokens, where the SDK exchanges the custom token for an ID Token. ID Tokens are also short live and will expire after one hour. When getting the ID Token, the SDK also receives a refresh Token which is used for refreshing the session ID Token. The ID token is used when doing authenticated requests to Firebase.
ID Token for verification: To get this token, you will need to call the function "getIDToken" (for Web). This function will return an ID Token that can be used only to verify requests coming from the client to your server. Similar to the other tokens, this one expires after one hour. When calling the getIDToken, the function will request a new one if the current is expired.
To verify the ID Token, you must use the mentioned token. If you try to use the session ID Token or the Custom token, you will get errors. The same applies if you try to use an expired ID Token.
All the calls and tasks to refresh the ID Tokens are handled by the SDKs behind the scenes.
It is not a good practice to store these tokens as they will expire after one hour, is better if you call the appropriate functions to get the latets tokens and pass the correct ID Token to be verified.
Lastly, you can find in these links the properties used for generating the custom token and the ID Token for verification.

Related

How can I authenticate to request Firebase REST api to fetch the list of firebase projects of a signed-in user?

I wish to fetch the list of firebase projects of a signed-in user (not me) on my website.
I can't seem to request the firebase rest api correctly (endpoint) as I receive a 401 UNAUTHENTICATED error.
The steps I follow:
I am using firebase.auth().signInWithPopup on my browser to authenticate users. Here are the scopes used:
// Google provider
const provider = new GoogleAuthProvider();
// Additionnal scopes
provider.addScope("https://www.googleapis.com/auth/cloud-platform");
provider.addScope("https://www.googleapis.com/auth/firebase");
provider.addScope("https://www.googleapis.com/auth/datastore");
// Signin
signInWithPopup(auth, provider);
I am then using getIdToken method to get a token from the user.
const token = await auth.currentUser.getIdToken(true);
I use this token to request https://firebase.googleapis.com/v1beta1/projects:
const data = await window.fetch("https://firebase.googleapis.com/v1beta1/projects", {
headers: {
authorization: `Bearer ${token}`,
},
})
.then(res => res.json());
but I am always getting a 401 error:
{
"error": {
"code": 401,
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
}
What am I doing wrong?
To get a list of Firebase projects for a user you need an OAuth2 token for a user that is a collaborator on the projects. You are trying to use the ID token/JWT for a Firebase Authentication user, which is not the same.
Follow the documentation on generating an access token to get value that you can use to get a list of projects.
I just found the answer, the firebase SDK does give you an accessToken with the idToken. You can then use this token instead of the idToken for any of your REST api needs.
const result = await signInWithPopup(auth, provider);
// This gives you a Google Access Token. You can use it to access the Google API.
const credential = GoogleAuthProvider.credentialFromResult(result);

Using the Google Sign-In bearer token on vanilla javascript web app hosted with firebase functions

It seems there are many issues surrounding this with very few results e.g. here with no definitive answer.
Using the javascript Google Sign-in method, which returns the OAuth credential which includes accessToken, User, etc - the token which should be present in all authenticated requests to the hosted app (where security is crucial).
Is the following a good solution for vanilla Javascript web apps client-side (in one of the .js files) and what issues may arise from it?
// 1. Sign-in
firebase.auth()
.signInWithPopup(provider)
.then((result) => {
// 2. upon successful sign-in
Axios({
url: "/user/register-token",
method: "POST",
headers: {authorization: `Bearer ${idToken}`},
data: { //empty }
}).then((result) => {
// 3. successfully registered & authenticated, proceed to authenticated dashboard route
window.location.assign('/user/dashboard');
}).catch((error) => { // handle error});
}).catch((error) => {
// 4. Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
console.log(error);
});
Steps:
Sign-in with Google signin (using popup)
On successful popup signin, send token to GET /user/register-token which takes the token, verifies it (with some additional email domain checks) and saves it to the session cookie
Upon successful token registered, go to authenticated page
Handle error
My logic:
Since the session is stored server side, but there are session Id's attached to the current browsing session, I can thus use this as a "password of sorts" to verify the firebase session.
Concerns:
The only concern is using any verified firebase token and registering it. This however is address (where I check the associated email and confirm it is part of a firebase user collection & it has a recognized and accepted domain). Thus, they are not confirmed and sent back to the login page with a HTTP/403.
Is my logic sound, are there any potential issues?
My solution was on the right track, and matched up quite well with Firebase's cookie session.
For the record, I had alot of trouble storing the bearer token in my express session, thus searched for other solutions.
According to my steps, the solutions follows the same footprint, but with a slightly different implementation:
client-side changes
The ID Token (bane of my existence for atleast 24 hours) can be retrieved using this solution.
TL;DR (client side)
firebase.auth()
.signInWithPopup(provider)
.then((result) => {
firebase.auth().currentUser
.getIdToken()
.then((idToken) => // use this idToken, NOT result.credential.idToken
//...
Finally, send that to your server to create a session, endpoint is of your choosing e.g. POST: /user/createSession
Server-side changes
You need an endpoint (mine is /user/sessionLogin - part of userRouter - see more more info). Using the code below, you can also include CSRF (I couldn't get mind to work on the client side...), createSessionCookie using the idToken above - this works perfectly w/o any issues.
router.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.
const auth = admin.auth();
auth.verifyIdToken(idToken).then(value => {
console.log("Token verified")
return 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'}));
}).catch((error) => {
console.error(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
});
}).catch(reason => {
console.error("Unable to verify token");
console.error(reason);
res.status(401).send('INVALID TOKEN!');
});
});
Finally, to validate your requests and check cookie status, you need to verify your requests to the server using (or request a new login session by redirecting to /user/login):
exports.authenticate = (req, res, next) => {
const sessionCookie = req.cookies.session || '';
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
return admin
.auth()
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
.then((decodedClaims) => {
req.user = decodedClaims;
next();
})
.catch((error) => {
console.error(error);
// Session cookie is unavailable or invalid. Force user to login.
req.flash("message", [{
status: false,
message: "Invalid session, please login again!"
}])
res.redirect('/user/login');
});
};
see this for more information.

Netlify Identity / GoTrue-js do user JWT expire?

Using https://github.com/netlify/gotrue-js to interface with netlify's authentication service (called "Identity") how frequently is it necessary to do the following:
const user = auth.currentUser();
const jwt = user.jwt();
jwt
.then(response => console.log("This is a JWT token", response))
.catch(error => {
console.log("Error fetching JWT token", error);
throw error;
});
Will the resulting JWT be valid forever? For the duration of the user's logged-in session? Or does it expire after a given amount of time?
Generally JWTs contain can (optionally) contain an exp (expiration) claim, that contains the time when it will expire.
I don't have experience with GoTrue, but according to their documentation you can configure the expiration, and it's set to a default value of 3600 seconds.
As the library also works with refresh tokens, you won't have to re-authenticate again after the token expires but use the refresh token to get a new access token.

How to authenticate API calls with Cognito using Facebook?

I want to authenticate users using Cognito, with option to use Facebook. User can sign_in/sign_up using either of those options.
I have created Cognito User Pool and Cognito Federated Identity, and also I have created Facebook App for authentication. Both User Pool and Facebook app are connected to Federated identity.
When I sign_up and later authenticate Cognito User via Cognito User Pool, then Cognito returns accessToken, which I store in localStorage on front and use whenever needed for athentication.
I have /authenticate endpoint (express), that takes in username & password, and returns accessToken if all went well. Whenever I make API call that requires auth, I send accessToken that I have in local storage. It goes, more or less as this:
// POST user/authenticate
const authenticationData = {
Username: username,
Password: password
}
authenticationDetails = new AuthenticationDetails(authenticationData)
const userData = {
Username: username,
Pool: userPool()
}
cognitoUser = new CognitoUser(userData)
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (res) => resolve(res), // here I get accessToken
onFailure: (err) => {
console.log('[authenticateUser error]', err)
reject(err)
},
//...
However
When I use Facebook, I do not get accessToken I could use in same fashion. I get accessToken from Facebook via FB.login, I pass it to Cognito to authenticate, and then I don't know what to do, because I cannot get any token that could be used to authenticate API calls, that require Cognito Authentication.
Here's what I do:
await window.FB.login((response) => {
props.userFacebookSignIn(response)
})
// ...
call(foo, 'users/facebook_sign_in', { accessToken: payload.facebookAccessToken })
// ...
// users/facebook_sign_in
AWS.config.region = config.AWSRegion
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'foo',
Logins: {
'graph.facebook.com': facebookAccessToken
}
})
AWS.config.credentials.get((err) => {
// Here I get no errors, I presume that I have logged Facebook user in
const accessKeyId = AWS.config.credentials.accessKeyId
const secretAccessKey = AWS.config.credentials.secretAccessKey
const sessionToken = AWS.config.credentials.sessionToken
// here I can do stuff probably,
// but I would like to receive token that would allow me to do stuff,
// rather than context I can do stuff in
})
While I am doing all of this, I have this feeling, that devs at AWS implemented Cognito as frontend solution, rather than something to be used in backend. Correct me if I am wrong.
Nevertheless, I would like to be able authenticate api calls using Cognito and Facebook interchangeably in express middleware.
Is that possible? Thanks.
I have used federated identity for salesforce single sign on but i imagine the steps will the same. After authenticating with facebook you will recieve and id_token from them in response. You have to pass this as a parameter in the getId method:
var params = {
IdentityPoolId: 'STRING_VALUE', /* required */
AccountId: 'STRING_VALUE',
Logins: {
'<IdentityProviderName>': 'STRING_VALUE',
/* 'graph.facebook.com': ... */
}
};
cognitoidentity.getId(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
In the result you will get an identity id which you can save somewhere so that you don't have to make this call everytime while authenticating. Now take this identity id and make the getCredentialsForIdentity call:
response = client.get_credentials_for_identity(
IdentityId='string',
Logins={
'string': 'string'
},
CustomRoleArn='string'
)
This will finally give you the temporary access key, secret key and session key you need.
I decided to use oAuth.
Here's quick & dirty look on how it's done
In AWS Cognito
1) Set up Cognito User Pool. Add App Client save App client id & App client secret as COGNITO_CLIENT_ID and COGNITO_CLIENT_SECRET
2) Go to Federation > Identity providers and add your Facebook app ID and App secret (both you will find in Facebook app panel)
3) Go to App integration > App client settings click "Select all", set up your Callback URL, mine is localhost:5000/facebook also select Authorization code grant and Allowed OAuth Scopes (save scopes to say: COGNITO_SCOPES)
4) Now go to App integration > Domain name and enter your custom domain; let's say example-app-debug so it's: https://example-app-debug.auth.us-east-1.amazoncognito.com
That's all there is to Cognito
no the Facebook part
5) Settings > Basic add example-app-debug.auth.us-east-1.amazoncognito.com to your App domains - Save Changes
6) In Facebook Login > Settings in Valid OAuth Redirect URIs add this URL: https://example-app-debug.auth.us-east-1.amazoncognito.com/oauth2/idpresponse and Save Changes
and the code
In browser, redirect user to this url when Login w. Facebook button is clicked:
window.location.href =
`https://example-app-debug.auth.us-east-1.amazoncognito.com/oauth2/authorize` +
`?identity_provider=Facebook` +
`&redirect_uri=http://localhost:5000/facebook` +
`&response_type=code` +
`&client_id=${COGNITO_CLIENT_ID}` +
`&scope=${COGNITO_SCOPES}`
this call should come back to you with a code, like this: http://localhost:5000/facebook?code=foo-bar-code Send this code to your backend.
In backend, do this:
const axios = require('axios')
const url = `` +
`https://${COGNITO_CLIENT_ID}:${COGNITO_CLIENT_SECRET}` +
`#example-app-debug.auth.us-east-1.amazoncognito.com/oauth2/token` +
`?grant_type=authorization_code` +
`&code=foo-bar-code` + // <- code that came from Facebook
`&redirect_uri=http://localhost:5000/facebook` +
`&client_id=${COGNITO_CLIENT_ID}`
const response = await axios.post(url)
// response should have access_token, refresh_token and id_token in data
You send access_token, refresh_token and id_token back to frontend and save them in local storage and use them to authenticate and Done.

Get Auth0 access_token from existing id_token

I'm using auth0 to authenticate my logins to my Single Page App (built on React). I'm mostly using the base API calls (listed here).
The process I'm using is:
get username/email and password when the user enters them on my app's login page
Send a POST request to /oauth/ro with those values - here is that code:
export const login = (params, err) => {
if (err) return err
const {email, password} = params
const {AUTH0_CLIENT_ID, AUTH0_DOMAIN} = process.env
return fetch(`${AUTH0_DOMAIN}/oauth/ro`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'client_id': AUTH0_CLIENT_ID,
'username': email,
'password': password,
'connection': 'Username-Password-Authentication',
'grant_type': 'password',
'scope': 'openid',
'device': '',
'id_token': ''
})
})
.then(response => response.json())
.then(json => {
const {id_token, access_token} = json
setCookieValue('id_token', id_token) // utility function I wrote
return getProfile(access_token)
.then(data => {
const {user_id, email: emailAddress, picture, name} = data
return {id_token, user_id, emailAddress, picture, name}
})
})
.catch(error => console.log(`ERROR: ${error}`))
}
This is all sent through Redux and the user is logged in (assuming the username/password was correct).
However, I'm trying to figure out how to persist the login when refreshing the page/coming back to the app. I'm saving the id_token (which is a JWT) in the browser's cookies and can fetch this when the app renders server-side. I can decode the JWT and get the payload (sub is the user ID from auth0). However, to get the profile data I need the access_token which Auth0 provides when using the /oauth/ro POST request. Obviously, if the JWT token has expired then it will just reject it and keep the user logged out.
Here is my code to decode the JWT (happens on app render):
const ID_TOKEN = req.cookies.id_token || false
if (ID_TOKEN) {
verifyJwt(ID_TOKEN, (err, decoded) => {
if (err) { console.log(`JWT Verification error: ${err}`) }
else {
const {sub} = decoded
getProfile(sub).then(data => store.dispatch(fetchUserDetails(data))) // fails as `sub` (the user id) is not the `access_token` which it requires
}
})
}
I have tried using the /oauth/ro call again, but this time specifying "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer" and using the id_token retrieved from the cookies, and specifying a device. However, when I do this call, I get this error from Auth0:
{
"error": "invalid_request",
"error_description": "there is not an associated public key for specified client_id/user_id/device"
}
So my question is, what API call do I need to make to get the access_token from the id_token JWT?
Also, as a bonus - when I do the POST request to login, the password is being transfered over plaintext. How would I encrypt this when sending to auth0 so they can decrypt it back? I assume it involves using the client_secret which auth0 provide but I'm not sure how to go about doing that.
The ability to refresh a token programmatically without any type of user interaction is accomplished through the use of refresh tokens. However, this is not applicable for browser-based applications because refresh tokens are long-lived credentials and the storage characteristics for browsers would place them at a too bigger risk of being leaked.
If you want to continue to use the resource owner password credentials grant you can choose to ask the user to input the credentials again when the tokens expire. As an alternative, upon authentication you can obtain the required user information and initiate an application specific session. This could be achieved by having your server-side logic create an application specific session identifier or JWT.
You can also stop using the resource owner password credentials grant and redirect the user to an Auth0 authentication page that besides returning the tokens to your application would also maintain an authenticated session for the user, meaning that when the tokens expired and your application redirected again to Auth0, the user might not need to manual reenter credentials because the Auth0 session is still valid.
In relation to the password being sent in plaintext; the resource owner endpoint relies on HTTPS so the data is encrypted at the protocol level. You must also use HTTPS within your own application for any type of communication that includes user credentials of any kind.
Also note that you can control what's returned within the ID token through the use of scopes, depending on the amount of information in question you might not even need to make additional calls to get the user profiles if you signal that you want that information to be contained within the ID token itself.

Categories