I've a meteor app which uses Neo4j as a database with neo4jreactivity driver. Since I'm not using Mongo, Meteor.loginWithPassword(email, password, function(err) {...}) doesn't work. My question is:
How do I define custom authentication rule to login to the app?
kind of like:
customLogin(email, password, function() {...});
You can use the Accounts.registerLoginHandler method to accomplish this. This function allows developers to add custom authentication methods. Check out https://meteorhacks.com/extending-meteor-accounts.html for a great article with more details.
You likely want to continue to use loginWithPassword, and register a loginHandler similar to the one in Meteor's accounts-password package (see
Meteor's implementation ), with the call to Meteor.users.findOne(selector) replaced with a database lookup in Neo4j.
If you want to use a custom login method, your code might look something like the code from here (modified for the purposes of this question). Note that this code is not complete, nor is it a secure means of authenticating:
// client-side
// This function can be called to log in your users, and will
// trigger the following
Meteor.loginWithNeo4j = function(email, password, callback) {
//create a login request with the email and password passed in
var loginRequest = {email: email, password: password};
//send the login request
Accounts.callLoginMethod({
methodArguments: [loginRequest],
userCallback: callback
});
};
// server-side
Accounts.registerLoginHandler(function(loginRequest) {
// loginRequest is a JS object that will have properties
// "email" and "password as passed in the client code
// -- here you can write code to fetch the user's ID from the database
// take a look at https://github.com/meteor/meteor/blob/devel/packages/accounts-password/password_server.js#L61
// to see how meteor handles password checking
return {
userId: userId
}
});
The accounts package in general has a lot of dependencies on MongoDB, but you should be able to piece together various methods from the package to get auth to work.
To fetch user's object use:
Meteor.neo4j.query('MATCH (a:Player {_id: "kE3ypH4Um"}) RETURN a').get().a[0]
/* or by email */
Meteor.neo4j.query('MATCH (a:Player {email: "name#domain.com"}) RETURN a').get().a[0]
Also see updated driver API
Related
I'm using Firebase auth to login with Facebook, Google and email/pass. Basically, everything runs client-side, I make a call to Firebase and I receive an object containing an access token (that is a JWT ID Token), a customer id and its email. When I get this object, I put it into a persistent store (local storage, I know it's bad) and I perform an API call to one of my sveltekit endpoint that will in turn make another API call to a backend API (written in Go) to get all the user informations: firstname, lastname, phone, address, stats etc. To give a little bit of context, below is a diagram to illustrate what's happening when a user sign-in using Facebook.
Up to now, I just put the Firebase object into a store and just check if the information are there to allow access to a particular page. This check is done in the +layout.svelte page of the directory containing the page to be protected. It looks like something like this:
onMount(() => {
// redirect if not authenticated
if (browser && !$authStore?.uid) goto(`/auth/sign-in`);
});
It's probably not a good thing especially since my store persists in the local storage and therefore is prone to some javascript manipulation.
From my understanding, there's at least 2 things that could be better implemented but I may be wrong:
Set the access token in an httponly cookie straight after receiving it from Firebase. This would avoid storing the access token (that is a JWT) in the local storage and It could be used to authorize access or not to some protected pages...
I receive the Firebase authentication object on client-side buthttp only cookie can only be created from server side. I thought about sending the object as a payload to a POST sveltekit endpoint (like /routes/api/auth/token/+server.js) and set the cookie from here but apparently cookies is not available in a sveltekit endpoint.
So, how and where should I set this cookie ? I see that cookies is available in the load function of a +layout.server.js file, as well as in the handle function of a hooks.server.js file, but I don't see the logic here.
Populate locals.userwith the authenticated user once I've performed a call to my backend. Well, here, it's not obvious to me because I think point 1) would be enough to manage access to protected pages, but I see that a check of locals.user is something I've seen elsewhere.
I tried to set locals.user in the sveltekit endpoint that is making the API call to the backend API:
// /routes/api/users/[uid]/+server.js
import { json } from "#sveltejs/kit";
import axios from "axios";
import { GO_API_GATEWAY_BASE_URL } from "$env/static/private";
export async function GET({ request, locals, params }) {
try {
const config = {
method: "get",
baseURL: GO_API_GATEWAY_BASE_URL,
url: `/users/${params.uid}`,
headers: {
Authorization: `Bearer ${uidToken}`, // <-- the Firebase ID Token
},
withCredentials: true,
};
const res = await axios(config);
// set locals
locals.user = json(res.data); // <--- DOESN'T SEEM TO WORK
return json(res.data);
} catch (error) {...}
}
...but in the +layout.server.js page that I've created I see nothing:
// routes/app/protected_pages/+layout.server.js
import { redirect } from "#sveltejs/kit";
export function load({ locals }) {
console.log(locals); // <----- contains an empty object: {}
if (!locals.user) throw redirect(302, "/auth/sign-in");
}
Thank you so much for your help
I am having difficulty using the FirebaseApp (a 3rd party API) to generate an authentication token that can be passed to a sidebar and used by the client to login and access my Firebase Database client-side.
I'm trying to use this tutorial but cannot get it working without using a database secret (which is being depreciated) in makeToken(). I'd prefer to use a service account as reflected in this tutorial. When I look at the difference between the tokens generated, the first 2 pieces separated by a '.' are identical, the last piece after the final '.' is different. The lengths are the same as well. eg:
//Example Generated by Database Secret: TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBv.ZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=.dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2U=
//Example Generated by Service Account: TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBv.ZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=.IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbml=
I can generate the OAuth access token, pass it to FirebaseApp and generate an authentication token, but when it is passed client-side and I attempt to authenticate I get an error: Login Failed! Error: INVALID_TOKEN: Failed to validate MAC.
It seems like there is a lot of misinformation and conflicting information on how this should be done.
I have a getFirebaseService() function server-side that uses Apps Script OAuth2 Library to get an access token.
function getFirebaseService() {
return OAuth2.createService('Firebase')
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(fb_PRIVATE_KEY) //Service account private key
.setIssuer(fb_SERVICE_EMAIL) //Service account email
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes.
.setScope('https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/firebase.database');
}
I have a makeToken() function server-side that gets an authentication token from Firebase using the OAuth access token. I am able to use the service.getAccessToken() OAuth token server-side to access and store data. So that works, I guess my issue is creating a client auth token that's more restrictive.
function makeToken(){
var service = getFirebaseService();
if (service.hasAccess()) {
return FirebaseApp.getDatabaseByUrl(fb_URL, service.getAccessToken()) //Database Secret Works: "AAslhfi3MYACCESSTOKEN2930hf03ah4th8" but is being depreciated.
.createAuthToken(Session.getActiveUser().getEmail());
} else {
Logger.log("makeToken: " + service.getLastError());
}
}
Then client-side, from the sidebar, I try to authenticate with a custom auth token retrieved server-side from makeToken().
var userAuthToken;
google.script.run.withSuccessHandler(function (requestAuthToken) {
userAuthToken = authenticateClient(requestAuthToken)
}).makeToken();
function authenticateClient(userRequestToken) {
var ref = new Firebase(fb_URL);
ref.authWithCustomToken(userRequestToken, function (error, authData) {
if (error) {
console.log("FB Login Failed!", error); //Error below come from here.
}
else {
console.log("FB Login Succeeded!", authData);
}
});
return ref.authData.auth;
}
This results in Login Failed! Error: INVALID_TOKEN: Failed to validate MAC..
Edit: Is it possible FirebaseApp is incorrectly generating the JWT Authentication Token?
Edit2: I think the above edit is unlikely as I attempted to use the GSApp library and had the same issue. It only seems to want the depreciated database secret, not a service account OAuth.
Alright, so after a very long day I figured it out. I'm going to lay out what I ended up using for libraries and what the issue was (see the third library). The main problem was essentially that the tutorial was outdated and no a lot of people use Firebase in apps script.
OAuth2 (Server-side)
Link
I didn't have to change anything here! It was working fine and never an issue.
FirebaseApp (Server-side)
Link
This is a nice library and I stuck with it because it worked well (once I got it there). I had to make a change to my original code that came from the tutorial I mentioned. My code ended up like this and worked:
if (service.hasAccess()) {
return FirebaseApp.getDatabaseByUrl(fb_URL, service.getAccessToken()) //get OAuth Token
.createAuthToken(Session.getEffectiveUser().getEmail(), null, serviceAccount.client_email, serviceAccount.private_key);
//... Added the null, private key, and service email parameters.
Firebase (Client-side)
Link
Alright, so this is where my main issue was -- The tutorial I followed for client-side setup was old. I had to upgrade the code on my own to use the new 3.x version:
<script src="https://www.gstatic.com/firebasejs/5.8.2/firebase.js"></script>
// Initialize Firebase
var config = {
apiKey: "<Web API Key>",
authDomain: "<Project ID>.firebaseapp.com",
databaseURL: "https://<DB URL>.firebaseio.com/"
};
firebase.initializeApp(config);
With this firebase instance I was able to update my original authenticateClient() method:
function authenticateClient(userRequestToken) {
firebase.auth().signInWithCustomToken(userRequestToken).catch(function(error) {
// Handle Errors here.
console.error("authClient: ", error.code, error.message);
});
return {
uid: firebase.auth().currentUser.uid,
metadata: {
lastSignInTime: firebase.auth().currentUser.lastSignInTime
}
};
}
That's it! I now have a firebase instance with a signed in user via JWT Custom Token! I came across a few people with similar issues an I hope this helps.
First of all, I am using nodejs for the backend. I use firebase hosting and firebase functions to deploy an express() app.
What I am trying to achieve is to make an admin website, which is connected to Firebase. so I have a route /admin/ like this:
adminApp.get("/", (request, response) => {
return response.redirect("/admin/login");
});
Here I basically want to check if a current user is logged in - or not.
I know firebase supports client side authentication using:
firebase.auth().onAuthStateChanged(user => {
if (user) {
} else {
}
});
And using
function login() {
var userEmail = document.getElementById("email").value;
var userPass = document.getElementById("password").value;
firebase.auth().signInWithEmailAndPassword(userEmail, userPass).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
if (error) {
document.getElementById('loginError').innerHTML = `Error signing in to firebase`;
}
});
}
However image this case:
Someone (not an admin) is visiting /admin/some_secret_website/ which he obviously does not have access to.
If I rely on client side authentication, it first loads the entire website and the scripts and then notices - hey I am not authenticated, let me redirect to /login. By then however anyone knows the source code of an admin page.
I'd rather have something like:
adminApp.get("/admin/some_secret_website", (request, response) => {
if (request.user) {
// user is authenticated we can check if the user is an admin and give access to the admin page
}
});
I know that you can get the user's token and validate that token using the AdminSDK, but the token must be send by the client code, meaning the website was already loaded.
I came across Authorized HTTPS Endpoint by firebase, but it only allows a middleware when using a bearer token.
Does anybody know how I can maintain a server side user object to not even return admin html to the browser but only allow access to admins?
Like Doug indicated, the way your admin website/webapp would function with Firebase Cloud Functions (which is effectively a Nodejs server) is that you get the request, then use the headers token to authenticate them against Firebase Auth. See this answer for a code snippet on this.
In your case, I'm thinking you would create a custom claim for an "administrator" group and use that to determine whether to send a pug templated page as a response upon authentication. As far as Authorization, your db rules will determine what said user can CRUD.
I am using signInWithCustomToken, after authentication I can not find where is stored my custom claims data which I have set in the server side(createCustomToken).
I can see them in firebase rules via auth.token, but how can I access them through firebase objects from within my javascript code.
The information in the token is not automatically available to your application code. But it is embedded in the token, so you can decode it yourself:
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace('-', '+').replace('_', '/');
return JSON.parse(window.atob(base64));
};
var user = firebase.auth().currentUser
user.getToken().then(data => {
console.log(parseJwt(data));
});
The function to parse the JWT comes from this question: How to decode jwt token in javascript
You'll note that it doesn't verify that the ID token is valid. That seems fine to me in client-side code, since the information will be used by the user themselves anyway. But if you do want to verify the token, you'll have to use a more involved method.
I have an application running on meteor.js and mongo.db. I am using robomongo as a tool for mongo.db. Now I'd like to do the following:
1. Somebody registers with my service (adding email to db)
2. I want to send an automatic welcome email to that person.
Is there any possibility how to do it?
You need an email server (SMTP), and then use the meteor email library. If you don't have an email server and don't want to create one, use a commercial solution. (Example)
Full working example you can find here: http://meteorpad.com/pad/iNMBHtNsv7XKHeq44
Notice it creates new users from within Meteor app, but the same effect will be when you use Robomongo or any other way of updating MongoDB.
First install package Email to be able to use Email.send.
In below example I assume that adding new user to collection Meteor.users should fire sending "invitation" email.
In very similar way you can detect if email was added to user object
(user.emails.length was changed) and then send email.
Then take a look at code:
// SERVER SIDE CODE:
Meteor.startup(function () {
// clean users on app resetart
// Meteor.users.remove({});
if(Meteor.users.find().count() === 0){
console.log("Create users");
Accounts.createUser({
username:"userA",
email:"userA#example.com",
profile:{
invitationEmailSend:false
}
}) ;
Accounts.createUser({
username:"userB",
email:"userB#example.com",
profile:{
invitationEmailSend:false
}
})
}
Meteor.users.find().observe({
added:function(user){
console.log(user.username, user.profile.invitationEmailSend)
if(!user.profile.invitationEmailSend){
Email.send({
from: "from#mailinator.com",
to: user.emails[0].address,
subject: "Welcome",
text: "Welcome !"
});
// set flag 'invitationEmailSend' to true, so email won't be send twice in the future ( ex. during restart of app)
Meteor.users.update({_id:user._id},{$set:{"profile.invitationEmailSend":true}});
}
}
})
});
Above code will send email to users who don't have flag equal to true in profile.invitationEmailSend. After e-mail is sent server updates user document in db and set user.profile.invitationEmailSend to true.
Whenever you add users to mongoDB (using Robomongo or any other way), then added function is executed and e-mail is send only to new users.