I have read tons of stackoverflow posts and docs, but I am still missing something. I am using Firebase realtime database. If I set rules to always true (no authentication needed) and remove interceptor everything is working fine.
Once interceptor is added and requests to Firebase are extended with either:
query param ?auth_token=<token here>
header Authorization: Bearer <token here>
I am getting 401 error (Unauthorized request.). Seems like my auth_token is not correct for some reason. Any idea why and how to fix this?
Thank you in advance!
Code below:
Auth service
import 'firebase/auth';
import 'firebase/database';
import { Injectable } from '#angular/core';
import { Router } from '#angular/router';
import * as firebase from 'firebase/app';
import { environment } from './../../environments/environment';
#Injectable({
providedIn: 'root'
})
export class AuthService {
public user: firebase.User;
public token: string;
constructor(private router: Router) {
this.initFirebase();
}
initFirebase() {
firebase.initializeApp({
apiKey: environment.firebase.apiKey,
databaseURL: environment.firebase.databaseURL
});
const auth = firebase.auth();
auth.onAuthStateChanged(firebaseUser => {
this.user = firebaseUser;
if (firebaseUser) {
firebaseUser.getIdToken().then(token => this.token = token);
} else {
this.token = null;
}
});
}
}
Firebase interceptor (tried providing auth token as either: header or query param)
import { HttpHandler, HttpInterceptor, HttpParams, HttpRequest } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { AuthService } from './auth.service';
#Injectable()
export class FirebaseAuthInterceptorService implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler) {
if (request.url.indexOf('firebaseio.com') === -1 || !this.auth.user) return next.handle(request);
const params = new HttpParams();
params.append('access_token', this.auth.token);
const newRequest = request.clone({
// headers: request.headers.append('Authorization', 'Bearer ' + this.auth.token)
setParams: {
access_token: this.auth.token
}
});
return next.handle(newRequest);
}
}
Related
I have create a guard to verify Okta tokens in my guard.However to verify there is another external api that needs to be called.Is it possible and advisable to call that api in the below guard? If yes,how can I go about implementing that?
import { Injectable, CanActivate, ExecutionContext, OnModuleInit } from '#nestjs/common';
import { Observable } from 'rxjs';
import * as OktaJwtVerifier from '#okta/jwt-verifier';
#Injectable()
export class OktaGuard implements CanActivate, OnModuleInit {
oktaJwtVerifier: any;
onModuleInit() {
this.oktaJwtVerifier = new OktaJwtVerifier({
issuer: 'https://{{host}}.okta.com/oauth2/default',
clientId: 'your_client_id'
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const token = context.getArgs()[0].headers.authorization.split(' ')[1];
return this.oktaJwtVerifier.verifyAccessToken(token, 'your_audience')
.then(() => {
return true;
})
.catch((err) => {
console.log(err);
return false;
});
}
}
I would like anonymous users to be able to only read and write their own data. I have the below as my security rules, but am getting a cannot read error in the simulator and the app.
I'm not sure that I'm going about it the right way. My main objective is to nest new assessments of the same user under their uid's and make it so they can only read, write and update their own assessments.
{
"rules": {
"users": {
"$uid": {
".write": "$uid === auth.uid";
".read": "$uid === auth.uid";
}
}
}
}
This is what my branch currently looks like
This is what I think it should look like to accomplish what I need.
Ideal Database structure
auth.gaurd.ts
import { Injectable } from '#angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '#angular/router';
import { AuthService } from "../../shared/service/auth.service";
import { Observable } from 'rxjs';
import * as firebase from 'firebase';
#Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
uid: string;
constructor(
public authService: AuthService,
public router: Router
){ }
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
this.authStateListener();
return true;
}
authStateListener() {
// [START auth_state_listener]
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
this.uid = user.uid;
console.log("user"+user.isAnonymous)
console.log("uid"+this.uid)
} else {
// User is signed out
return firebase.auth().signOut().then(() => {
localStorage.removeItem('user');
this.router.navigate(['sign-in']);
})
}
});
}
}
auth.service.ts
import { Injectable, NgZone, ViewChild, ElementRef, Component } from '#angular/core';
import { User } from "../service/user";
import { auth } from 'firebase/app';
import { AngularFireAuth } from "#angular/fire/auth";
import { AngularFirestore, AngularFirestoreDocument } from '#angular/fire/firestore';
import { ActivatedRoute, Router } from "#angular/router";
import * as firebase from 'firebase';
import "firebase/auth";
#Injectable({
providedIn: 'root'
})
export class AuthService {
userData: any; // Save logged in user data
#ViewChild('btnLogin') btnLogin: HTMLButtonElement;
constructor(
public afs: AngularFirestore, // Inject Firestore service
public afAuth: AngularFireAuth, // Inject Firebase auth service
public router: Router,
private actRoute: ActivatedRoute,
public ngZone: NgZone // NgZone service to remove outside scope warning
) {
}
anonymousSignIn(){
firebase.auth().signInAnonymously()
.then(()=>{
this.router.navigate(['assessment']);
console.log("clicking")
}).catch((error) => {
var errorCode = error.code;
var errorMessage = error.message;
console.log("error here")
});
}
**This is the code to push, read, update and delete branches in Firebase. The ReadAssessment list should display all data that the anonymous user owns in order for them to read it. ** fire-assessment.service.ts
import { AuthGuard } from './../shared/guard/auth.guard';
import { Injectable } from '#angular/core';
import {AngularFireDatabase, AngularFireList, AngularFireObject} from '#angular/fire/database';
import * as firebase from 'firebase';
import {Business} from '../models/business';
import { ActivatedRoute, Router } from '#angular/router';
import { map } from 'rxjs/internal/operators/map';
import { isNgTemplate } from '#angular/compiler';
#Injectable({
providedIn: 'root'
})
export class FireAssessmentService {
assessmentsRef: AngularFireList<any>; // Reference to Assessment data list, its an Observable
assessmentRef: AngularFireObject<any>; // Reference to assessment object
public database = firebase.database();
public UserAssessmentInput;
public ref;
public actRoute: ActivatedRoute;
public router: Router;
public auth: AuthGuard;
constructor(private db: AngularFireDatabase) {
}
CreateAssessment(business: Business ){
const key = this.database.ref('/users').push().key;
this.database.ref('/users').child(key).set(
///this.assessmentsRef.ref('/users').push(
{
busiName: business.busiName
});
}
ReadAssessment(id: string){
this.assessmentRef = this.db.object('users/' + id);
return this.assessmentRef;
}
ReadAssessmentsList(){
this.assessmentsRef = this.db.list('users/');
return this.assessmentsRef;
}
UpdateAssessments (business: Business){
this.assessmentRef.update({
busiName: business.busiName
});
}
DeleteAssessment(){
this.assessmentRef = this.db.object('users/');
this.assessmentRef.remove();
}
business.ts
export interface Business {
$key: string;
busiName: string;
}
Right now you're creating data with this:
const key = this.database.ref('/users').push().key;
this.database.ref('/users').child(key).set({
busiName: business.busiName
});
When you call push() Firebase generates a new unique location, which is the key starting with -M... in your JSON.
That value is not the UID of the current user, so these rules then don't allow the user to read or write it:
"users": {
"$uid": {
".write": "$uid === auth.uid";
".read": "$uid === auth.uid";
}
}
Instead you should write the data under a node using the user's UID as the key. That'd look something like:
const key = this.database.ref('/users').push().key;
if (firebase.auth().currentUser) {
const key = firebase.auth().currentUser.uid;
this.database.ref('/users').child(key).set({
busiName: business.busiName
});
}
else {
console.error("No current user while trying to write business name");
}
I am new in angular and firebase and trying to get users data to table only after succesful auth - if you log in correctly, the table shows the data, and if not, you can't see this data. I've tried to make simple firebase login and logout in my AuthService:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { AngularFireAuth } from '#angular/fire/auth';
import { Router } from '#angular/router';
import * as firebase from 'firebase/app';
#Injectable({
providedIn: 'root',
})
export class AuthService {
constructor(private router: Router, private fireAuth: AngularFireAuth) {}
onLogin(email: string, password: string) {
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(function () {
console.log('Succes');
})
.catch(function (error) {
console.log(error);
});
}
async onLogout() {
try {
await firebase.auth().signOut();
this.router.navigate(['./']);
} catch (error) {
console.log(error);
}
}
}
And this is my UserService responsible for displaying users in the table:
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import { User } from 'src/app/models/user.model';
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root',
})
export class UserService {
constructor(private http: HttpClient) {}
fetchUsers() {
let result = new Subject<User[]>();
this.http
.get('https://fir-login-1416c.firebaseio.com/users.json')
.subscribe((users) => {
let usersAr = Object.keys(users).map((id) => new User(users[id]));
result.next(usersAr);
});
return result.pipe(take(1));
}
addUser(user: User) {
let postData: User = user;
this.http
.post<{ name: string }>(
'https://fir-login-1416c.firebaseio.com/users.json',
postData,
{
observe: 'response',
}
)
.subscribe(
(responseData) => {
console.log(responseData.body.name);
},
(error) => {
console.log(error);
}
);
this.fetchUsers();
}
deleteUser() {
// Later
}
}
My firebase database rules looks like this:
{
"rules": {
".write": "auth !== null",
".read": "auth !== null"
}
}
But probelm is when I log in with the correct data and navigate to page with users table I see empty table and console shows
this errors.
It looks like the authentication didn't work at all or I just did something wrong.
If you have any suggestions on how to do this, give them to me :) Thanks!
You need to send some kind of authorisation headers with your request. Otherwise the database doesn't really know that your are authenticated.
I would suggest to use AngularFire not only for authentication but also for fetching data.
In a project, I work with 2 HTTP interceptors: 1 to add a JWT token to each request, the other to intercept an incoming 401 error status.
I call a separate program to get all feedback for my app in this service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { environment } from '#environments/environment';
import { Feedback } from '#app/_models/feedback';
#Injectable({ providedIn: 'root' })
export class FeedbackService {
constructor(
private http: HttpClient
) {}
getAll() {
return this.http.get<Feedback[]>(`${environment.apiUrl}/feedback`);
}
getById(id: string) {
return this.http.get<Feedback>(`${environment.apiUrl}/feedback/${id}`);
}
delete(id: string) {
return this.http.delete(`${environment.apiUrl}/feedback/${id}`);
}
}
The JWT interceptor:
import { Injectable } from '#angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '#angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '#environments/environment';
import { AuthorizationService } from 'src/shared/authorization.service';
#Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private auth: AuthorizationService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add auth header with jwt if user is logged in and request is to the api url
const authenticatedUser = this.auth.getAuthenticatedUser();
if (authenticatedUser == null) {
return;
}
authenticatedUser.getSession( (err, session) => {
if (err) {
console.log(err);
return;
}
const isApiUrl = request.url.startsWith(environment.apiUrl);
const token = session.getIdToken().getJwtToken();
const headers = new Headers();
headers.append('Authorization', token);
if (this.auth.isLoggedIn() && isApiUrl) {
request = request.clone({
setHeaders: {
Authorization: token,
}
});
}
return next.handle(request);
});
}
}
The Error interceptor:
import { Injectable } from '#angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '#angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AccountService } from '#app/_services';
#Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private accountService: AccountService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log(next.handle(request));
return next.handle(request).pipe(catchError(err => {
if (err.status === 401) {
// auto logout if 401 response returned from api
this.accountService.logout();
}
const error = err.error.message || err.statusText;
return throwError(error);
}));
}
}
When I provide both interceptors in my app.module,
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
I always get an error saying the following below. This happens because next.handle(request) apparently is undefined, and I don't really know why. Using only the Error interceptor works with no issue.
ERROR TypeError: Cannot read property 'pipe' of undefined
at ErrorInterceptor.intercept (error.interceptor.ts:14)
at HttpInterceptorHandler.handle (http.js:1958)
at HttpXsrfInterceptor.intercept (http.js:2819)
at HttpInterceptorHandler.handle (http.js:1958)
at HttpInterceptingHandler.handle (http.js:2895)
at MergeMapSubscriber.project (http.js:1682)
at MergeMapSubscriber._tryNext (mergeMap.js:46)
at MergeMapSubscriber._next (mergeMap.js:36)
at MergeMapSubscriber.next (Subscriber.js:49)
at Observable._subscribe (subscribeToArray.js:3)
Using only the JwtInterceptor gives following error, which I can't figure out where it's coming from. Of course, I would want to use both. Am I missing something while configuring the multiple interceptors?
ERROR TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
at subscribeTo (subscribeTo.js:27)
at subscribeToResult (subscribeToResult.js:11)
at MergeMapSubscriber._innerSub (mergeMap.js:59)
at MergeMapSubscriber._tryNext (mergeMap.js:53)
at MergeMapSubscriber._next (mergeMap.js:36)
at MergeMapSubscriber.next (Subscriber.js:49)
at Observable._subscribe (subscribeToArray.js:3)
at Observable._trySubscribe (Observable.js:42)
at Observable.subscribe (Observable.js:28)
at MergeMapOperator.call (mergeMap.js:21)
Rewrite your JwtInterceptor:
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable, from } from 'rxjs';
import { environment } from '#environments/environment';
import { AuthorizationService } from 'src/shared/authorization.service';
#Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private auth: AuthorizationService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return from(this.getSessionWithAuthReq(request, next));
}
async getSessionWithAuthReq(request: HttpRequest<any>, next: HttpHandler){
const authenticatedUser = this.auth.getAuthenticatedUser();
if (authenticatedUser) {
const authRequest: HttpRequest<any> = await new Promise( (resolve) => {
authenticatedUser.getSession( (err, session) => {
if (err) {
console.log(err);
// want to go on without authenticating if there is an error from getting session
return resolve(request);
}
const isApiUrl = request.url.startsWith(environment.apiUrl);
const token = session.getIdToken().getJwtToken();
const headers = new Headers();
headers.append('Authorization', token);
if (this.auth.isLoggedIn() && isApiUrl) {
const req = request.clone({
setHeaders: {
Authorization: token,
}
});
return resolve(req);
}
return resolve(request);
});
});
return next.handle(authRequest).toPromise();
}
return next.handle(request).toPromise();
}
}
Initially, I had a function that simply checked for the presence of a token and, if it was not present, sent the user to the login header. Now I need to implement the logic of refreshing a token when it expires with the help of a refreshing token. But I get an error 401. The refresh function does not have time to work and the work in the interceptor goes further to the error. How can I fix the code so that I can wait for the refresh to finish, get a new token and not redirect to the login page?
TokenInterceptor
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "#angular/common/http";
import {Injectable, Injector} from "#angular/core";
import {AuthService} from "../services/auth.service";
import {Observable, throwError} from "rxjs";
import {catchError, tap} from "rxjs/operators";
import {Router} from "#angular/router";
import {JwtHelperService} from "#auth0/angular-jwt";
#Injectable({
providedIn: 'root'
})
export class TokenInterceptor implements HttpInterceptor{
private auth: AuthService;
constructor(private injector: Injector, private router: Router) {}
jwtHelper: JwtHelperService = new JwtHelperService();
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.auth = this.injector.get(AuthService);
const accToken = this.auth.getToken();
const refToken = this.auth.getRefreshToken();
if ( accToken && refToken ) {
if ( this.jwtHelper.isTokenExpired(accToken) ) {
this.auth.refreshTokens().pipe(
tap(
() => {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${accToken}`
}
});
}
)
)
} else {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${accToken}`
}
});
}
}
return next.handle(req).pipe(
catchError(
(error: HttpErrorResponse) => this.handleAuthError(error)
)
);
}
private handleAuthError(error: HttpErrorResponse): Observable<any>{
if (error.status === 401) {
this.router.navigate(['/login'], {
queryParams: {
sessionFailed: true
}
});
}
return throwError(error);
}
}
AuthService
import {Injectable} from "#angular/core";
import {HttpClient, HttpHeaders} from "#angular/common/http";
import {Observable, of} from "rxjs";
import {RefreshTokens, Tokens, User} from "../interfaces";
import {map, tap} from "rxjs/operators";
#Injectable({
providedIn: 'root'
})
export class AuthService{
private authToken = null;
private refreshToken = null;
constructor(private http: HttpClient) {}
setToken(authToken: string) {
this.authToken = authToken;
}
setRefreshToken(refreshToken: string) {
this.refreshToken = refreshToken;
}
getToken(): string {
this.authToken = localStorage.getItem('auth-token');
return this.authToken;
};
getRefreshToken(): string {
this.refreshToken = localStorage.getItem('refresh-token');
return this.refreshToken;
};
isAuthenticated(): boolean {
return !!this.authToken;
}
isRefreshToken(): boolean {
return !!this.refreshToken;
}
refreshTokens(): Observable<any> {
const httpOptions = {
headers: new HttpHeaders({
'Authorization': 'Bearer ' + this.getRefreshToken()
})
};
return this.http.post<RefreshTokens>('/api2/auth/refresh', {}, httpOptions)
.pipe(
tap((tokens: RefreshTokens) => {
localStorage.setItem('auth-token', tokens.access_token);
localStorage.setItem('refresh-token', tokens.refresh_token);
this.setToken(tokens.access_token);
this.setRefreshToken(tokens.refresh_token);
console.log('Refresh token ok');
})
);
}
}
In your example you never subscribe to your refreshTokens().pipe() code. Without a subscription, the observable won't execute.
req = this.auth.refreshTokens().pipe(
switchMap(() => req.clone({
setHeaders: {
Authorization: `Bearer ${this.auth.getToken()}`
}
}))
)
This will first call refreshToken and run the tap there, then emit request with the new this.auth.getToken(), note that accToken still have old value as the code is not rerun.
You have to do something like that:
const firstReq = cloneAndAddHeaders(req);
return next.handle(firstReq).pipe(
catchError(
err => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401 || err.status === 403) {
if (firstReq.url === '/api2/auth/refresh') {
auth.setToken('');
auth.setRefreshToken('');
this.router.navigate(['/login']);
} else {
return this.auth.refreshTokens()
.pipe(mergeMap(() => next.handle(cloneAndAddHeaders(req))));
}
}
return throwError(err.message || 'Server error');
}
}
)
);
The implementation of cloneAndAddHeaders should be something like this:
private cloneAndAddHeaders(request: HttpRequest<any>): HttpRequest<any> {
return request.clone({
setHeaders: {
Authorization: `YourToken`
}
});
}