Skip to content

Instantly share code, notes, and snippets.

@shadow1349
Last active May 1, 2020 07:55
Show Gist options
  • Save shadow1349/a83fb4437c596c75d0c4a9c3df43d3ae to your computer and use it in GitHub Desktop.
Save shadow1349/a83fb4437c596c75d0c4a9c3df43d3ae to your computer and use it in GitHub Desktop.
Simple way to separate classes of users with Firebase
/**
* I like Angular, so this is the example I'll give you. It should
* still be relatively easy to read
*/
import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { switchMap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor(private auth: AngularFireAuth, private http: HttpClient) {}
/**
* Custom user claims will appear in the token
* the token will be sent to your API, if you're familiar
* with Angular you can use this in an HTTP Interceptor, otherwise
* you can read w/e framework/libraries documentation for sending headers in
* an HTTP request
*/
GetIDToken() {
/**
* All we need to do it grab the user object from firebase so we can call the getIdToken() method
* since this is an Angular project it just happens to be an observable, but if you're not using Angular
* it will probably be a promise or a callback
*/
return this.auth.authState.pipe(
switchMap(user => {
if (!user) {
return;
}
/**
* setting true here is important because it forces the refresh of the token
* so you can set claims and have them be applied without a user having to sign out
* and back in. But you can easily just remove the parameter if you want users to sign out
* and back in
*/
return user.getIdToken(true);
})
);
}
Login(email: string, password: string) {
return this.auth.auth.signInWithEmailAndPassword(email, password);
}
/**
* If you're curious as to why there is no header being set here read up on
* Angular HTTP Interceptors.
*
* Since our endpoint uses the firebase admin SDK to validate the token and the custom claims
* there is little we need to do here other then send the ID Token given to us from firebase
*/
RegisterNewEndUser(email: string, password: string) {
return this.http.post(`https://us-central1-my-project.cloudfunctions.net/api/endusers/create`, {
email, password
});
}
SignOut() {
return this.auth.auth.signOut();
}
}
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userid} {
allow read, update, delete: if isOwner(userid) || isAdmin();
// Only admins can create an end-user which means that
// our userCreated function will only run and set the
// endUser customer claim restricting access
// we can also limit a user's ability to list everyone in the
// collection so that only admins can do that operation.
allow create, list: if isAdmin();
}
match /admins/{adminid} {
allow read, update, delete: if isOwner(adminid);
allow list: if isAdmin();
// It sounds like you were trying to build a closed application for a single company
// rather than something that many companies could sign up for. If this is not the case
// let me know and I will modify my example to better fit that use case.
// the basic idea here is that if you're building a closed application for a single company
// you can simply assign one, or many, people to be superAdmins at the start. From there you can
// allow them to send out emails with a signup link.
allow create: if isSuperAdmin() || isAdmin();
}
// CUSTOM FUNCTIONS
function isOwner(uid) {
return request.auth.uid == uid;
}
function isAdmin() {
return request.auth.token['admin'] == true;
}
function isEndUser() {
return request.auth.token['endUser'] == true;
}
function isSuperAdmin() {
return request.auth.token['superAdmin'] == true;
}
}
}
/**
* Generally it is a good idea to break this up into seperate files
* but just so you can see all the functions code in one place I've
* put all the code into a single file
*/
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as express from 'express';
import * as cors from 'cors';
import * as bodyParser from 'body-parser';
// This will be called automatically when a user is added to the users collection
export const userCreated = functions.firestore
.document('users/{userid}')
.onCreate((snapshot, context) => {
/**
* When we sign up a user we're going to use their UID from firebase auth to create
* their firestore document, this allows us to easily connect users in firebase auth
* to their corresponding firestore document.
*
* Custom claims is a dynamic object so it can be whatever you want
*/
return admin.auth().setCustomUserClaims(context.params.userid, {endUser: true});
});
// We'll do the same thing as above for admins, except set them as an admin in custom claims
export const adminCreated = functions.firestore
.document('admins/{adminid})
.onCreate((snapshot, context) => {
return admin.auth().setCustomUserClaims(context.params.adminid, {admin: true});
});
/**
* It sounds like you were trying to build a closed application for a single company
* rather than something that many companies could sign up for. If this is not the case
* let me know and I will modify my example to better fit that use case.
*/
// Just for show this will be here, but this can also be in RTDB or in Firestore
const authorizedDomains = ['mydomain.com', 'myOtherDomain.com'];
/**
* a few simple lines of code and no one outside of your specified domains
* can sign up at all
*/
export const newUserSignup = functions.auth.user()
.onCreate((user, context) => {
const domain = user.email.split('@')[1];
/**
* You could do a RTDB/Firestore lookup of the domain if you chose to go that route
* however, only people signing up with domains you specify will be able to
*/
if(authorizedDomains.indexOf(domain) === -1) {
admin.auth.deleteUser(user.uid);
}
/**
* This will disable a user until their email is verified stopping them from being able to login
* if, for example, someone was clever and figured out your authorized domains they would still need
* to be able to verify their email address.
*/
return admin.auth().updateUser(user.uid, {disabled: !user.emailVerified});
});
/**
* We can use a trigger to clean up after ourselves
* when a user is removed
*/
export const newUserSignup = functions.auth.user()
.onDelete((user, context) => {
if(user.customClaims['admin']) {
return admin.firestore().document(`admins/${user.uid}`).delete();
}
else {
return admin.firestore().document(`users/${user.uid}`).delete();
}
});
/**
* Create a regular express api
*/
const app = express();
// And your CORS issue is solved
const options: cors.CorsOptions = {
origin: true
};
/**
* Setup App
*/
app.use(cors(options));
app.disable('x-powered-by');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
/**
* We can fully authenticate requests using the admin SDK
* if you look at authService you can see the GetIDToken() method
* which will give you a JWT token to send here
*/
app.use('/', async (req: express.Request, res: express.Response, next: express.NextFunction) => {
let token: string = '';
if (req.headers.authorization && req.headers.authorization.toString().startsWith('Bearer ')) {
token = req.headers.authorization.toString().split('Bearer ')[1];
} else {
return res.status(401);
}
const decodedToken = await admin
.auth()
.verifyIdToken(token)
.catch(err => {
res.status(403);
return null;
});
if(decodedToken && decodedToken['admin']) {
// We can set the decodedToken on the request to re-use later
req['user'] = decodedToken;
next();
return;
} else {
return res.status(401);
}
});
app.post('/endusers/create', async (req: express.Request, res: express.Response) => {
const user = await admin.auth().createUser({email: req.body.email, password: req.body.password});
await admin.firestore().document(`users/${user.uid}`).set({
// set whatever properties you want
});
return res.status(200);
});
export const api = functions.https.onRequest(app);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment