08 aprilie 2020

Angular - ziua 9 - Autentificare

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}

Logoutthis.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

Niciun comentariu: