Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Custom Usernames With Firebase Authentication and Angular

Episode 16 written by Jeff Delaney
full courses and content on fireship.io

Firebase authentication is super convenient, but you can’t easily assign custom usernames out of the box. In this lesson, we are going to give users custom usernames and asynchronously validate their availability during the signup process. On every keyup, the username will be checked for duplicates, so we can display a helpful message to the user.

When a user signs in via OAuth, the app will see if they have a username set. If they do NOT, it will keep them on the login component and force them to enter a username, which will validate username availability asynchronously.

ng g service auth
ng g component user-login
ng g component choose-username

Modeling the Username Data

Our database will have a collection of users for general record keeping, but the quickest way to asynchronously check username availability is to save all usernames in their own collection, with the keys being the usernames that are not available to new users.

users
userId
username: string

usernames
${username}: userId

For example…

users
idxyz123
username: "CoolUserXYZ"

usernames
CoolUserXYZ: idxyz123

AuthService that Verifies Username Availability

First, let’s create a User class to simplify the auth object. We only care about the uid and the username. As a constructor, it will take the Firebase AuthState from angularfire2.

We need to subscribe to both the authState and the user information in the database at the same time… So how do we handle nested subscriptions with RxJS? In this case, we are going to use switchMap, which will emit the Firebase auth object first, then get the user values from the database, returning everything as an observable. Using switchMap avoids nested RxJS subscriptions (generally considered a bad practice).

auth.service.ts

import { Injectable } from '@angular/core';
import { AngularFireDatabase } from 'angularfire2/database';
import { AngularFireAuth } from 'angularfire2/auth';
import * as firebase from 'firebase';

import 'rxjs/add/operator/switchMap';

export class User {

uid: string;
username: string = "";

constructor(auth) {
this.uid = auth.uid
}

}


@Injectable()
export class AuthService {

currentUser: User;

constructor(private afAuth: AngularFireAuth,
private db: AngularFireDatabase) {

this.afAuth.authState.switchMap(auth => {
if (auth) {
this.currentUser = new User(auth)
return this.db.object(`/users/${auth.uid}`)
} else return [];
})
.subscribe(user => {
this.currentUser['username'] = user.username
})

}

googleLogin() {
const provider = new firebase.auth.GoogleAuthProvider()
return this.afAuth.auth.signInWithPopup(provider)
.then(() => console.log('successful auth'))
.catch(error => console.log(error));
}
}

We need to make sure the username is available before it can be selected. First, the username collection is queried with the user’s text input. Querying with this method only targets a single key value pair, rather than an entire list, so it’s much faster. In fact, it’s almost instantaneous in the UI as you will see. We also use a getter to check if the current user has a username. Here’s how the remainder of the service is filled out.

get hasUsername() {
return this.currentUser.username ? true : false
}

checkUsername(username: string) {
username = username.toLowerCase()
return this.db.object(`usernames/${username}`)
}

updateUsername(username: string) {

let data = {}
data[username] = this.currentUser.uid

this.db.object(`/users/${this.currentUser.uid}`).update({"username": username})
this.db.object(`/usernames`).update(data)
}

Login Component with Username Validation

async username validation with firebase demo

We will run a query to Firebase after each keydown event to see if there’s a matching username in the database. If not, the user can go ahead and select it.

user-login.component.ts

import { Component } from '@angular/core';
import { AuthService } from "../../core/auth.service";


@Component({
selector: 'user-login',
templateUrl: './user-login.component.html',
styleUrls: ['./user-login.component.scss']
})
export class UserLoginComponent {

usernameText: string;
usernameAvailable: boolean;

constructor(public auth: AuthService) { }


checkUsername() {
this.auth.checkUsername(this.usernameText).subscribe(username => {
this.usernameAvailable = !username.$value
})
}

updateUsername() {
console.log
this.auth.updateUsername(this.usernameText)
}


signInWithGoogle() {
this.auth.googleLogin()
}
}

A new user will start by authenticating with Google. When that is successful, the form input to select a username is displayed.

When there’s a matching username, the usernameAvailable variable is set to false. In the template, we use this variable to display a success or error message. It is also used to disable the submit button.

user-login.component.html

<h1>Login</h1>
<button (click)="signInWithGoogle()" class="button btn-social btn-google"
*ngIf="!auth.currentUser">
<i class="fa fa-google-plus fa-lg"></i> Connect Google
</button>

<button type="button" class="button"
*ngIf="auth.currentUser"
(click)="logout()">
Logout
</button>

<div *ngIf="auth.currentUser && !auth.hasUsername">

<h3>Choose a Username</h3>

<input type="text" class="input" placeholder="choose a username"
[(ngModel)]="usernameText"
(keyup)="checkUsername()">

<p class="help is-success" *ngIf="usernameAvailable && usernameText">
@{{usernameText}} is available
</p>

<p class="help is-danger" *ngIf="!usernameAvailable && usernameText">
@{{usernameText}} has already been taken
</p>

<button class="button is-primary"
[disabled]="!usernameAvailable || !usernameText"
(click)="updateUsername()">

Select Username

</button>

</div>

Validating a Unique Username via Firebase Database Rules

The current username validation is great as a frontend UX feature, but it is still vulnerable to accidental duplication. Let’s add an extra layer security by creating a firebase database rule that ensures a username cannot be accidentally duplicated on the backend.

database.rules.json

"users": {
".write": "auth != null",
"username": {
".validate": "!root.child('usernames').child(newData.val()).exists()"
}
},

"usernames": {
".write": "auth != null"
}

That’s it for custom usernames with firebase.