Firebase verifyIdToken + NodeJS Express Authentication design - swift

Problem:
Due to legislation I have to store personal information within the EU (Social security number). Therefore I can't store this information in Firebase since there is no guarantee of geographical datacenter location when using Google's cloud services.
My proposed solution:
Having a Redis key value store with the sensitive information that can be accessed via a simple REST api where user authentication would be achieved using the users ID token, sent via HTTP Headers.
Firebase allows for verification of a user via the verifyIdToken method in the NodeJS library. This would allow me to check if the user ID matches any user id in my /admin end point of my Firebase. (Or I could hardcode the userIDs that would be allowed into the server since there aren't that many.)
So, the flow of the request would be as follows:
User signs in client side using the Firebase SDK.
Whenever the user needs access to the sensitive information it first gets the user's ID token
let currentUser = FIRAuth.auth()?.currentUser
currentUser?.getTokenForcingRefresh(true) {idToken, error in
if let error = error {
return
}
let headers = [
"X-FBUser-Token":idToken
]
//build request here to https://myServer.com/myEndpoint
}
Then server side we would retrieve the request
app.get('/myEndpoint', function(req, res) {
let idToken = req.get('X-FBUser-Token')
verifyToken(idToken, function(isAdmin){
if (isAdmin) {
//Fetch the key value pair and send it back to the client here
}
})
})
function verifyToken(idToken, cb) {
firebase.auth().verifyIdToken(idToken).then(function(decodedToken) {
var uid = decodedToken.sub;
firebase.database().ref('admins/' + uid).on('value', function (snap){
cb(snap.val() !== null)
})
}).catch(function(error) {
// Handle error
});
}
And then the client would receive back the response and deal with it. Everything done over HTTPS ofcourse.
Note: I know that the code above is rather crude and would need some refinement, but hopefully you get the concept
My questions:
First of all, is this a safe way of doing things?
Is there a better, more straight forward approach?

Related

Meteor - Password recovery / Email confirmation dynamic url

Basically, I'm using the accounts-base package on meteor and on meteor startup, I set up what template the server should use for the password recovery mail, email confirmation mail, etc.
For example, in my server/startup.js on meteor startup I do many things like :
Accounts.urls.verifyEmail = function (token) {
return Meteor.absoluteUrl(`verify-email/${token}`);
};
Accounts.emailTemplates.verifyEmail.html = function (user, url) {
return EmailService.render.email_verification(user, url);
};
The problem is that my app is hosted on multiple host names like company1.domain.com, company2.domain.com, company3.domain.com and if a client wants to reset his password from company1.domain.com, the recovery url provided should be company1.domain.com/recovery.
If another client tried to connect on company2.domain.com, then the recovery url should be company2.domain.com.
From my understanding, this is not really achievable because the method used by the Accounts Package is "Meteor.absoluteUrl()", which returns the server ROOT_URL variable (a single one for the server).
On the client-side, I do many things based on the window.location.href but I cannot seem, when trying to reset a password or when trying to confirm an email address, to send this url to the server.
I'm trying to find a way to dynamically generate the url depending on the host where the client is making the request from, but since the url is generated server-side, I cannot find an elegent way to do so. I'm thinking I could probably call a meteor server method right before trying to reset a password or create an account and dynamically set the ROOT_URL variable there, but that seems unsafe and risky because two people could easily try to reset in the same timeframe and potentially screw things up, or people could abuse it.
Isn't there any way to tell the server, from the client side, that the URL I want generated for the current email has to be the client current's location ? I would love to be able to override some functions from the account-base meteor package and achieve something like :
Accounts.urls.verifyEmail = function (token, clientHost) {
return `${clientHost}/verify-email/${token}`;
};
Accounts.emailTemplates.verifyEmail.html = function (user, url) {
return EmailService.render.email_verification(user, url);
};
But I'm not sure if that's possible, I don't have any real experience when it comes to overriding "behind the scene" functionalities from base packages, I like everything about what is happening EXCEPT that the url generated is always the same.
Okay so I managed to find a way to achieve what I was looking for, it's a bit hack-ish, but hey..
Basically, useraccounts has a feature where any hidden input in the register at-form will be added to the user profile. So I add an hidden field to store the user current location.
AccountsTemplates.addField({
_id: 'signup_location',
type: 'hidden',
});
When the template is rendered, I fill in this hidden input with jQuery.
Template.Register.onRendered(() => {
this.$('#at-field-signup_location').val(window.location.href);
});
And then, when I'm actually sending the emailVerification email, I can look up this value if it is available.
Accounts.urls.verifyEmail = function (token) {
return Meteor.absoluteUrl(`verify-email/${token}`);
};
Accounts.emailTemplates.verifyEmail.html = function (user, url) {
const signupLocation = user.profile.signup_location;
if (signupLocation) {
let newUrl = url.substring(url.indexOf('verify-email'));
newUrl = `${signupLocation}/${newUrl}`;
return EmailService.render.email_verification(user, newUrl);
}
return EmailService.render.email_verification(user, url);
};
So this fixes it for the signUp flow, I may use the a similar concept for resetPassword and resendVerificationUrl since the signupLocation is now in the user profile.
You should probably keep an array of every subdomains in your settings and keep the id of the corresponding one in the user profile, so if your domain changes in the future then the reference will still valid and consistent.

make meteor restful api/web-service

I have created a new url/route in my app where I need to write a web-service. I need to write a service that deletes user according to the parameters passed in the service. For now, anyone should be able to call that service (will make it secure at later stage). App is built on meteor.
My url is : loaclhost:3000/deleteUser. Now one should be able to call my delete user function defined on this page and pass json structure data as an argument to it. If the data is valid, then the user should be deleted.
Using simple:rest package
Meteor.publish("delUser", function (a, b) {
UserDetails.remove({}); //delete user according to data received
}, {
url: "/testing/delUser", //url where third party will call the function
getArgsFromRequest: function (request) {
// Let's say we want this function to accept a form-encoded request
// with fields named `a` and `b`.
console.log('received : ' + JSON.stringify(request.body) );
var content = request.body;
// Since form enconding doesn't distinguish numbers and strings, we need
// to parse it manually
return [content.a, content.b];
}
})
How to access the function, delUser from a thrid party? I also need to add authentication at a later stage.
Personnally, I use this :
simple:rest
simple:json-routes
simple:rest-accounts-password
I find it easier to implement.
even iron:router comes with server side routes where you can build your own functions and api calls.
http://iron-meteor.github.io/iron-router/#restful-routes
Sample (Server side code) :
Router.map(function () {
this.route("api", {path: "/api/:paramsYouNeed",
where: "server",
action: function(){
this.response.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
if (this.request.method == 'POST') {
var response;
//do whatever you want to do
this.response.end(response);
}
}
});
The other user can call this by making a http.post request to the above url (http:www.a****a.com/api/params)
The easiest way to do this is use the restivus package.
https://atmospherejs.com/nimble/restivus
Restivus makes building REST APIs in Meteor 0.9.0+ easier than ever
before! The package is inspired by RestStop2 and Collection API, and
is built on top of Simple JSON Routes to provide:
A simple interface for creating REST APIs
Easy setup of CRUD endpoints for Mongo Collections
User authentication via the API
Optional login and logout endpoints
Access to this.user in authenticated endpoints
Custom authentication if needed
Role permissions for limiting access to specific endpoints
Works alongside the alanning:roles package - Meteor's accepted role permission package

Can I get a consistent 'iss' value for a Google OpenIDConnect id_token?

I'm using Google's OpenIDConnect authentication, and I want to validate the JWT id_token returned from Google. However, the documentation seems inconsistent about what value Google returns for the iss (issuer) claim in the ID token.
One page says, "iss: always accounts.google.com", but another page says "The value of iss in the ID token is equal to accounts.google.com or https://accounts.google.com" and a comment in the example code further explains:
// If you retrieved the token on Android using the Play Services 8.3 API or newer, set
// the issuer to "https://accounts.google.com". Otherwise, set the issuer to
// "accounts.google.com". If you need to verify tokens from multiple sources, build
// a GoogleIdTokenVerifier for each issuer and try them both.
I have a server-side application, not an Android app, so I'm not using Play Services.
To further muddy the waters, the OpenIDConnect specification itself contains a note that:
Implementers may want to be aware that, as of the time of this writing, Google's deployed OpenID Connect implementation issues ID Tokens that omit the required https:// scheme prefix from the iss (issuer) Claim Value. Relying Party implementations wishing to work with Google will therefore need to have code to work around this, until such time as their implementation is updated. Any such workaround code should be written in a manner that will not break at such point Google adds the missing prefix to their issuer values.
That document is dated November 8, 2014. In the time since then, has Google standardized on an iss value, or do I really need to check for both of them? The comment above seems to indicate that only Play Services >=8.3 gets iss with https://, and everywhere else the value will be just accounts.google.com. Is that true?
You have to check both possibilities. This is what worked for me...
Decode the token to get the issuer. If the issuer is not equal to either one of https://accounts.google.com or accounts.google.com you can stop there. It's an invalid token.
If the issuer is equal to either of the above Google strings, then pass that same decoded issuer value forward to the verification step.
Following is the an implementation I wrote in JavaScript for some Node.js Express middleware:
function authorize(req, res, next) {
try {
var token = req.headers.authorization;
var decoded = jwt.decode(token, { complete: true });
var keyID = decoded.header.kid;
var algorithm = decoded.header.alg;
var pem = getPem(keyID);
var iss = decoded.payload.iss;
if (iss === 'accounts.google.com' || iss === 'https://accounts.google.com') {
var options = {
audience: CLIENT_ID,
issuer: iss,
algorithms: [algorithm]
}
jwt.verify(token, pem, options, function(err) {
if (err) {
res.writeHead(401);
res.end();
} else {
next();
}
});
} else {
res.writeHead(401);
res.end();
}
} catch (err) {
res.writeHead(401);
res.end();
}
}
Note this function uses jsonwebtoken and jwk-to-pem node modules. I ommitted details of the getPem function which ultimately converts a json web key to pem format.
To start with, I definitely agree that Google's documentation is a murky business.
There are a couple of different ways in which you can validate the integrity of the ID token on the server side (btw this is the page you're looking for):
"Manually" - constantly download Google's public keys, verify signature and then each and every field, including the iss one; the main advantage (albeit a small one in my opinion) I see here is that you can minimize the number of requests sent to Google).
"Automatically" - do a GET on Google's endpoint to verify this token - by far the simplest:
https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}
Using a Google API Client Library - the overhead might not be worth it, C# doesn't have an official one etc.
I suggest you go with the 2nd option and let Google worry about the validation algorithm.

Firebase: Authenticate an existing user using REST API and Firebases hidden Auth URL

For the past 3 years we have used HTML/Js only with Firebase but now we are using Unity as well.
The current Unity/Firebase only works on Android/iOS when deployed and 99% of our work is on the windows store.
I've actually got a pretty decent Unity/Firebase codebase going but it requires me to use a full App Secret.
All the other libraries expose a method to login with Email/Password but the REST API only allows the use of a token or your app secret that it then states is ill advised to put into your client; I guess the thinking is if you're using a different library that you'll have your own auth/user method which we don't...
Now, I've pulled apart the web version and got this:
https://auth.firebase.com/v2/<myfirebase>/auth/password?&email=dennis%40<mysite>&password=<mypassword>v=js-2.2.9&transport=json&suppress_status_codes=true
So there IS an endpoint that I can send stuff to and I've tested it inside unity with good results.
Obviously the URL isn't guaranteed to stay working but I'm wondering if there is any reason NOT to use this?
Also, Why not just expose this endpoint in the official REST API?
As I understand it, that URL will continue to work for your Legacy Firebase project. You will have to do the same sort of reverse engineering if you want to update to the new Firebase 3.0 API. However, if you are still using a legacy Firebase project -- I encourage you to take a look at this. It has not been updated to work with Firebase 3.0 -- so I needed to do something similar to what you did to allow login to the new API.
I was able to do this with the new API using C# as follows (where FirebaseManager is a Singleton I wrote for Global variables and functions to write and read from/to the DB :
Hashtable loginData = new Hashtable();
loginData.Add ("email", <EMAIL-GOES-HERE>);
loginData.Add ("password", <PASSWORD-GOES-HERE>);
loginData.Add ("returnSecureToken", true);
UnityHTTP.Request loginRequest = new UnityHTTP.Request ("post",
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key="
+ <YOUR-PROJECT-API-KEY-GOES-HERE>, loginData);
loginRequest.Send ((request) => {
Hashtable jsonResponse = (Hashtable)JSON.JsonDecode(request.response.Text);
if (jsonResponse == null) {
DisplayErrorMessage("Error logging in. Server returned null or malformed response");
}
FirebaseManager.Instance.idToken = (string)jsonResponse["idToken"]; // This is your auth token
FirebaseManager.Instance.uid = (string)jsonResponse["localId"]; // this is your "uid"
});
// I have a list of users in my db keyed by the "uid" -- I access them like this
UnityHTTP.Request fullnameRequest = new UnityHTTP.Request ("get",
<YOUR-DATABASE-ROOT-URL-HERE>
+ "/users/" + FirebaseManager.Instance.uid + ".json?auth=" + FirebaseManager.Instance.idToken);
fullnameRequest.Send ((request) => {
Debug.Log(request.response.Text);
Hashtable jsonResponse = (Hashtable)JSON.JsonDecode(request.response.Text);
if (jsonResponse == null) {
DisplayErrorMessage("Error getting user info. Server returned null or malformed response");
}
FirebaseManager.Instance.fullname = (string)jsonResponse["fullname"];
FirebaseManager.Instance.groupId = (string)jsonResponse["group"]; // just storing this in memory
});
So I don't think there is any harm in using the URL, just make sure you budget time for more work when things change.

How to deal with anonymous login attempts when you allow both anonymous access and logged in users?

I have a framework that allows anonymous access as well as named user accounts. I'm exposing an OData resource on a certain URL. When configured as such, anonymous users can see parts of the resource and logged in users (through basic authentication) can see more.
The problem I'm facing is that some OData clients (like Excel) will initially attempt to access the OData resource anonymously even when you do provide credentials. Only when this fails, they will use the provided credentials. My understanding is that this is because there are many ways to log in and some clients just always try the most basic option first. But this prevents them from ever actually seeing more data, because they never use the provided credentials and also never get the authentication challenge when the resource allows anonymous access.
Is there a way to solve this issue, allowing both anonymous access AND properly sending an authentication challenge when possible? Is there maybe some header that clients will send when they do have credentials but just aren't supplying them initially?
Some (scala) code to make this a bit more tangible:
val (username, password) = getAuthInfo(request)
if (username != null && password != null) {
val regularSession = integration.core.login(username, password)
logger.debug(s"Login OK: User '$username' (Number of concurrent sessions: ${integration.core.getNumberConcurrentSessions}).")
(IMxRuntimeResponse.OK, null, regularSession)
} else if (integration.configuration.getEnableGuestLogin) {
val guestSession = integration.core.initializeGuestSession
logger.debug(s"Anonymous user '${guestSession.getUser.getName}' created " +
"(Number of concurrent sessions: ${integration.core.getNumberConcurrentSessions}).")
(IMxRuntimeResponse.OK, null, guestSession)
} else {
val responseMessage = "No basic authentication in header."
logger.warn(s"Login failed: $responseMessage")
(IMxRuntimeResponse.UNAUTHORIZED, responseMessage, null)
}
Somewhere else outside the surrounding try/catch:
if (httpStatusCode == IMxRuntimeResponse.UNAUTHORIZED)
response.addHeader("WWW-Authenticate", "Basic")
As you can see the challenge is never sent when anonymous access is allowed.
Edit: we investigated and there does not seem to be anything special in the headers of this request that would indicate this is an initial attempt that will result in another request when an authentication challenge is sent, rather than just another anonymous login attempt. We are at a loss here now on how to proceed.