POSTS
Firebase Custom Authentication System with Django
At Ontruck we use the Django framework for one of our services. This “automagically” implies that almost anything we end up doing is done in Python (backend). I have always liked Django development and used it in a fair amount of projects. However years go by and new tech stack and tools appear. Then they mature to the point where they’re stable and “production ready”.
Lately I have being assigned the task of building a web app prototype for a specific group of users. And the tech stack decision was mostly up to our team. Which is great news. So we ended up choosing:
- React JS
- Bootstrap
- Firebase
Me and my colleague Vadim have previously tested the authentication system integration between our current Django project and Firebase. Sure, we did it mainly of curiosity. The other reason was the feeling that we could enable Firebase use in our team. This feeling, some previous experience and the positive outcome of the test led us selecting Firebase as the way to go.
Firebase Custom Authentication System
Both Django framework and Firebase have their own authentication systems. In this particular case our users are already in the Django app. So we had to take this in to account and make their auth experience as seamless as we could.
One of Firebases auth systems is the so called Custom Authentication System. It delegates the actual authentication to a 3rd party system. This in turn allows the users to use their existing credentials and sign in seamlessly in to the new Firebase app.
JSON Web Token
The cornerstone of this authentication system is the JSON Web Token (JWT). In case you are unfamiliar with the JWT concept I would advise you to check out the link above. Basically JWT is a standard that defines a way for securely transmitting information between systems as a JSON object. I won’t go deep into how it works, because it is a topic on its own. And because I am only a user of these kind of substances.
Claims
Along with the token we may pass a number of extra data. This extra data is called claims. Claims can be verified because they are part of JWT.
Communication sequence
There are 3 different parties:
- Client web app
- Django app
- Firebase
The client web app initiates the whole process by submitting user credentials to the Django app. Django authenticates the user. If successful it calls a Firebase admin SDK method to get the JWT and sends it back to the client. The client app calls the Firebase client SDK method with this token. If successful the user will be signed in.
Example
Because of the NDA I’m pretty sure I cannot share our real life code so I will create a demo app and share the most crucial parts.
I have put together two repositories with both, the backend Django and frontend React apps:
Django
Let’s say our Django users have a profile with tenant id set. And we want to provide this id as a claim within the JWT. Our UserProfile model could look like this:
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete='CASCADE')
tenant = models.ForeignKey(Tenant, on_delete='CASCADE')
This is an example of a class based authentication view we could implement to authenticate our users and return the JWT with all the required claims:
class AuthenticateView(APIView):
def post(self, request, format=None):
serializer = AuthenticationSerializer(data=request.data)
if serializer.is_valid():
username = serializer.validated_data['username']
password = serializer.validated_data['password']
user = authenticate(request, username=username, password=password)
if user is not None:
additional_claims = {
'tenant': str(user.userprofile.tenant.id)
}
custom_token = auth.create_custom_token(
str(user.id), additional_claims)
ts = TokenSerializer({'token': custom_token.decode('UTF-8')})
return Response(ts.data, status=status.HTTP_200_OK)
return Response(status=status.HTTP_406_NOT_ACCEPTABLE)
Client app
The client app is a React web app. The only Django app API endpoint it uses is the authentication one.
The POST request is sent from within a hook:
const useFetchJWTFromDjangoApp = credentials => {
const [authResponseData, setAuthResponseData] = useState({
JWT: "",
error: false
});
useEffect(() => {
if (credentials) {
fetch("/api/authenticate/", {
method: "post",
body: JSON.stringify(credentials),
headers: {
"Content-Type": "application/json"
}
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response bad.");
})
.then(data => {
setAuthResponseData({ JWT: data.token, error: false });
})
.catch(error => {
return setAuthResponseData({ JWT: "", error: true });
});
}
}, [credentials]);
return authResponseData;
};
If successful, the response will contain the JWT. There are several things we could do:
- use the token to call Firebase auth signInWithCustomToken method
- parse the token and extract the tenant id
- persist tenant id
This is exactly what we will do:
const useSignInWithCustomToken = (JWT, firebase) => {
useEffect(() => {
if (JWT && firebase) {
firebase
.signInWithToken(JWT)
.then(() => {
const base64Url = JWT.split(".")[1];
const decodedValue = JSON.parse(atob(base64Url));
localStorage.setItem("tenantId", decodedValue.claims.tenant);
})
.catch(function(error) {
console.error("Error logging in: ", error);
});
}
}, [JWT, firebase]);
};
If “signInWithCustomToken” resolves the user auth state changes. We can listen to this event:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
console.log("User authenticated");
} else {
console.log("User lost");
}
});
Firestore security rules
We included the tenant id in the token for a reason. Let’s say client app will be fetching some data from Firestore. The path to that data is designed in such way that all the valuable contents is stored within a doc named after the tenant id:
Client app would be getting the data:
db.collection("secretStuff")
.doc(tenantId)
.collection("phones")
.get();
Now when writing down the security rules it is possible to read the provided earlier claims. They are available on the “auth.token” variable. This makes client side queries reliable:
service cloud.firestore {
match /databases/{database}/documents {
match /secretStuff/{tenantId}/phones {
allow write: if auth.token.tenant == tenantId;
}
}
}
Final word
I’ve always tried to avoid things that seemed a bit difficult to approach. Integrating Firebase custom auth system with Django authentication was one of these things. However this example illustrates that it’s not as hard as thought beforehand.
Thanks to the teamwork we did the job of testing this integration in almost no time. I finally learned some basic JWT related stuff and got to use Firebase at Ontruck.
Thanks for reading and feel free to ping me on twitter.