Last active
May 1, 2020 07:55
-
-
Save shadow1349/a83fb4437c596c75d0c4a9c3df43d3ae to your computer and use it in GitHub Desktop.
Simple way to separate classes of users with Firebase
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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