I am trying to find/create a proper (the best) way to get and use custom claims in an Angular app. I added an admin custom claim via a cloud function. What I want to have (and what I tried to do until) now is:
- Get the claims (and the logged in user) in one service (e.g.
auth.service
) - Allow all other components who need to read the claims to do so via a simple API from that service
- Make no other components subscribe to authState or anything else (just read, synchronously, the attributes of my
auth.service
)
Why do I want this? - because I believe it is more readable and easier to maintain
(by only reading (subscribing to) the authState
in one place (e.g. authService.ts
), thus making maintenance easier and allowing other components to synchronously read the claims from the authService.ts
attributes/fields)
So, the code for what I am doing now (which is not working... see POINTS_IN_CODE):
auth.service.ts
// imports omitted for brevity...
@Injectable()
export class AuthService {
user: Observable<User> = of(null);
uid: string;
claims: any = {};
claimsSubject = new BehaviorSubject(0);
constructor(private afAuth: AngularFireAuth,
private afStore: AngularFirestore,
private functions: AngularFireFunctions) {
this.afAuth.authState
.subscribe(
async authUser => {
if (authUser) { // logged in
console.log(`Auth Service says: ${authUser.displayName} is logged in.`);
this.uid = authUser.uid;
this.claims = (await authUser.getIdTokenResult()).claims;
// POINT_IN_CODE_#1
this.claimsSubject.next(1);
const userDocumentRef = this.afStore.doc<User>(`users/${authUser.uid}`);
// if provider is Google (or Facebook <later> (OR any other 3rd party))
// document doesn't exist on the first login and needs to be created
if (authUser.providerData[0].providerId === 'google.com') {
userDocumentRef.get()
.subscribe( async snapshot => {
if ( ! snapshot.exists) { // if the document does not exist
console.log(`\nNew document being created for: ${authUser.displayName}...`); // create a user document
await userDocumentRef.set({name: authUser.displayName, email: authUser.email, provider: 'google.com'});
}
});
}
this.user = userDocumentRef.valueChanges();
}
else { // logged out
console.log('Auth Service says: no User is logged in.');
}
}
);
}
login(email, password): Promise<any> {
return this.afAuth.auth.signInWithEmailAndPassword(email, password);
}
hasClaim(claim): boolean {
return this.hasAnyClaim([claim]);
}
hasAnyClaim(paramClaims): boolean {
for (let paramClaim of paramClaims) {
if (this.claims[paramClaim]) {
return true;
}
}
return false;
}
}
login.component.ts
// imports...
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
form: FormGroup;
hide = true;
errorMessage = '';
loading = false;
constructor(private fb: FormBuilder,
public authService: AuthService,
private router: Router) {}
ngOnInit() {
this.logout();
this.form = this.fb.group({
username: ['test@test.te', Validators.compose([Validators.required, Validators.email])],
password: ['Asdqwe123', Validators.compose([Validators.required])]
});
}
submit() {
this.loading = true;
this.authService.login(this.form.value.username, this.form.value.password)
.then(resp => {
this.loading = false;
// POINT_IN_CODE_#2
// what I am doing right now, and what doesn't work...
this.authService.user
.subscribe(
resp => {
if (this.authService.hasClaim('admin')) {
this.router.navigate(['/admin']);
}
else {
this.router.navigate(['/items']);
}
}
);
// POINT_IN_CODE_#3
//this.authService.claimsSubject
// .subscribe(
// num => {
// if (num === 1) {
// if (this.authService.hasClaim('admin')) {
// this.router.navigate(['/admin']);
// }
// else {
// this.router.navigate(['/items']);
// }
// }
// });
}
logout() {
this.authService.logout();
}
}
POINTS_IN_CODE
In auth.service.ts
at POINT_IN_CODE_#1
- I had the idea to emit from this subject claimsSubject
and in login.component.ts
at POINT_IN_CODE_#3
subscribe to it and know that, if it has a value of 1
, the claims have been retrieved in auth.service.ts
from the authState
.
In login.component.ts
at POINT_IN_CODE_#2
I know that I could get the claims from resp.getIdTokenResult
but it just doesn't "feel" right... which is what this question is about, mostly...
The concrete question I could be asking is this:
I want to be able to redirect the user after login to the admin
page if he has the 'admin' custom claims.
I would like to do it, as I stated above (if possible AND if it is good/improving-readability/improving_maintainability), without subscribing to the authState
directly, but through some "thing" from the auth.service.ts
.
I would use the same "logic" to make, for example, an AuthGuard
which would just call authService.hasClaim('admin')
, and not have to subscribe to authState
itself to do the check.
N.B. I want to know if the way I did it is good, if it has any caveats or just simple improvements. All suggestions and comments are welcome, so please do comment, especially on my Why do I want this? part!
Edit-1: Added typescript code highlighting and pointed out the exact place in my code that doesn't work the way I want.
Edit-2: Edited-out some comments regarding reasons why my authService.user was null... (I had some code run which set it to null before it was checked in the login component...)