Autentificare si protejarea rutelor
- RESTful API is stateless => nu se poate folosi sesiunea, ci: clientul va trimite datele de identificare, iar serverul raspunde cu un token. Clientul salveaza acel token si il foloseste in cereri ulterioare catre server.
- Pregatire server Firebase:
{
"rules": {
".read": 'auth != null',
".write": 'auth != null'
}
}
Authentication -> Set up sign-in method -> Email/password -> Enable
Lucrul cu cereri de autentificare in Firebase:
- se copiaza url-ul generic pentru sign up de la Firebase (la momentul de fata este https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=)
- se adauga la el API key-ul aflat in project overview (Web API Key)
- se trimite o cerere de POST catre acel url nou-format, cu datele: {email: your-email, password: your-password, returnSecureToken: true}
- cererea de POST trebuie sa fie de tipul: post<AuthResponseData>, unde tipul raspunsului este definit de Firebase si este tradus de noi intr-o interfata Typescript astfel:
interface AuthResponseData {
kind: string;
idToken: string;
email: string;
refreshToken: string;
expiresIn: string;
localId: string;
registered?: boolean; // optional: only for the sign in request
}
- se procedeaza asemanator si pentru log in - folosind celalalt URL specific (https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=) si aceeasi cheie
Variabile cu underscore
export class User {
constructor(public email: string, public id: string,
private _token: string, private _tokenExpirationDate: Date) {
}
get token() {
if (!this._tokenExpirationDate || new Date() > this._tokenExpirationDate) {
return null;
}
return this._token;
}
}
Ce inseamna:
- se poate accesa pe variabila user.token pentru a executa metoda de get
-
user.token nu se poate asigna
- metoda
get token (getter) defineste pasii pentru obtinerea token-ul
Construirea unui utilizator (Sign up):
return this.httpClient
.post<AuthResponseData>(url, this.getAuthData(email, password))
.
pipe(
catchError(this.handleError),
tap(resData => {
// resData contine expiresIn, adica nr de secunde pt care e valabil token-ul
const expirationDate = new Date(new Date().getTime() + +resData.expiresIn * 1000);
// resData mai contine si id-ul local in Firebase, cat si token-ul
const user = new User(resData.email, resData.localId, resData.idToken, expirationDate);
this.user.next(user);
}));
// unde user: Subject<User>;
- se procedeaza si la
login la fel ca la
signup.
Operatori rxjs (lista completa
aici)
*
pipe - un fel de stream() din Java 8, poate primi ca argument alti operatori rxjs, se poate chema pe un Observable, ca rezultat aduce fluxul de valori
* tap - un fel de forEach din Java, se foloseste cu pipe ... ex:
clicks.pipe(tap(ev => console.log(ev))); Poate avea efecte laterale, daca schimbi ceva in ev.
*
map - asemanator cu map din Java, primeste o singura valoare din sir si poate face ceva cu ea, fara efecte laterale ... ex:
from([1, 2, 3]).pipe(map(item => item + 2)).subscribe(item => console.log(item));
*
take - primeste ca argument un numar N, emite doar primele N valori emise de Observabil, nu asculta la infinit ... ex:
interval(1000).pipe(take(3)).subscribe(x => console.log(x)); // 0, 1, 2
*
exhaustMap - asteapta primul Observable sa termine, apoi ia urmatoarea valoare (???)
ex:
const exhaustSub = firstInterval.pipe(
exhaustMap(f => { console.log(''); return secondInterval; })
);
return this.authService.user.pipe(take(1), exhaustMap(user => {
const urlWithToken = this.URL + '?auth=' + user.token;
return this.httpClient.get<Recipe[]>(urlWithToken);
})
BehaviorSubject - un Subject care da acces si la valoarea anterioara emisa (cu next), chiar daca nu a fost facut subscribe la acel moment (este folosit cu
exhaustMap)
Exemplu:
user = new BehaviorSubject<User>(null); // null e prima valoare initiala
Interceptori
- se poate folosi pentru a atasa fiecarei cereri token-ul de care are nevoie
- se creeaza ca serviciu:
@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
constructor(private authService: AuthService) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.authService.user.pipe(take(1),
exhaustMap(user => {
if (!user) {
return next.handle(req); // unchanged
}
// adds token to all outgoing requests
const modifiedReq = req.clone({params: new HttpParams().set('auth', user.token)});
return next.handle(modifiedReq);
})
);
}
}
- se adauga in app.module.ts -> providers, ca:
{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true}
Logout:
this.user.next(null);
De asemenea se curata Local Storage de orice informatie despre user: localStorage.clear();
Alternativ, daca vrem sa stergem doar informatia adaugata explicit despre user: localStorage.removeItem('userData');
Auto-login: prin cookies sau prin Local Storage API furnizat de browser
Pentru varianta Local Storage API:
1. se cheama in Typescript: localStorage.setItem('userData', JSON.stringify(user)); pentru a salva datele despre utilizator la momentul crearii lui
2. intr-o metoda autoLogin() din serviciul de autentificare, se obtin datele inapoi prin: JSON.parse(localStorage.getItem('userData')); Se reconstituie utilizatorul si se emite BehaviorSubject-ului user (recomandabil daca apeland token() nu se primeste null, deci daca inca are token-ul valid).
3. in app.component.ts se implementeaza OnInit, iar in ngOnInit() se cheama metoda autoLogin() din serviciul de autentificare
Auto-logout: la expirarea token-ului (default dupa o ora in cazul Firebase).
1. se creeaza o metoda in serviciul de autentificare in care, in plus, salvam timer-ul intr-o variabila globala:
autoLogout(duration: number) {
this.tokenExpirationTimer = setTimeout(() => {
this.logout();
}, duration);
}
2. aceasta metoda este chemata de fiecare data cand se face login sau auto-login in serviciu
3. in metoda de logout() avem grija sa distrugem timer-ul:
if (this.tokenExpirationTimer) {
clearTimeout(this.tokenExpirationTimer);
}
this.tokenExpirationTimer = null;
Protejarea rutelor - unele pagini nu a ar trebui sa fie accesibile utilizatorilor nelogati
1. se creeaza un nou serviciu care implementeaza CanActivate. Metoda canActivate() va returna un boolean, corespunzand permisiunii de a accesa o anumita pagina (true pentru utilizatorii logati).
@Injectable({providedIn: 'root'})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
: Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.authService.user.pipe(map(user => !!user)); // true if user exists, false otherwise
}
}
2. in app-routing.module.ts, pe linia componentei care ne intereseaza se adauga: canActivate: [AuthGuard]
3. Pentru redirectare in functie de permisiune, se adauga:
return this.authService.user.pipe(
map(user => !!user), // true if user exists, false otherwise tap(auth => {
if (!auth) {
this.router.navigate(['/auth']);
}
}));
Alternativ cu UrlTree (un alt output posibil al metodei
canActivate()):
return this.authService.user.pipe(
take(1),
map(user => {
const isAuth = !!user;
if (isAuth) {
return true;
}
return this.router.createUrlTree(['/auth']);
}));
!!! take(1) s-a folosit pentru a lua doar ultimele valori introduse, pentru ca garda sa nu continue sa asculte la infinit