konta użytkowników w angular

Konta użytkowników w .NET Core 2.1 i Angular 6 [część 1]

Intro

Obsługa kont użytkowników to coś, co pojawia się w większości aplikacji. Zalogowanie się daje możliwość skorzystania z określonych funkcji systemu, w zależności od roli nadanych użytkownikowi.

Proces logowania to inaczej uwierzytelnianie. Proces kontroli, czy dany użytkownik ma uprawnienia (odpowiednie role) nazywamy autoryzacją.

Niemalże każdy framework do tworzenia serwisów internetowych udostępnia wbudowany system uwierzytelniania i autoryzacji. Dla ASP.NET MVC jest to np. Identity. Co jednak w sytuacji, gdy nasz front-end jest niezależny od back-endu?

How to

Aby stworzyć system, który pozwoli logować się w aplikacji napisanej w Angularze i mieć dostęp do zasobów .NET Core’owego serwera, użyjemy serwisu Auth0. Dzięki podejściu Authentication as a Service cały mechanizm przechowywania informacji o użytkownikach, przydzielania im ról, itd znajduje się w chmurze u dostawcy.

Firma Auth0 stworzyła standard JWT (ang. Json Web Token), który służy do uwierzytelniania i autoryzacji w serwisach komunikujących się protokołem HTTP(S).  Działa to tak, że każde zapytanie zawiera nagłówek z tokenem, który  jednoznacznie identyfikuje użytkownika, opisując między innymi jego role.

Aby dowiedzieć się więcej o samym JWT odsyłam do strony jwt.io. W wielkim skrócie chodzi o to, że token jest bardzo kompaktowy, dzięki czemu umieszczenie go w każdym HTTP requeście nie obciąża sieci tak bardzo jak inne formy autoryzacji (np. SAML używający XML).

W tym wpisie pokażę jak stworzyć i skonfigurować aplikację na portalu Auth0, a w kolejnym jak odpowiednio skonfigurować aplikację.

Gotowy projekt do pobrania z repozytorium + instrukcja uruchomienia znajdują się tu: https://github.com/MattKoboski/dotnetcorelovesangular

Get to work

1. Rejestracja konta

Po wejściu na stronę https://auth0.com/ należy nacisnąć Sign In w prawym górnym rogu, by się zarejestrować. Osobiście preferuję logowanie przy pomocy konta GitHub.

2. Dodanie aplikacji

W kolejnych krokach trzeba stworzyć aplikację. Kliknięcie NEW APPLICATION, podanie nazwy oraz wybranie typu SPA jest pierwszym zadaniem.

1 - dodanie aplikacja

Następnie należy wybrać Angular 2+ i przejść do ustawień aplikacji, klikając Application Settings w sekcji Get Your Application Keys.

2 - dodanie aplikacja

3. Konfiguracja aplikacji Angular

Struktura wygląda następująco:

app
├─ services
│  │─ auth-guard.service.ts
│  │─ auth.service.ts
│  │─ auth0-variables.ts
│  └─ authHttpServiceFactory.ts
│─ app.component.css
│─ app.component.html
│─ app.component.ts
└─ app.module.ts

Pozostaje otworzyć konsolę w oknie aplikacji front-endowej (tam, gdzie znajduje się plik package.json) i przy pomocy NPM zainstalować auth0-js:

npm install --save auth0-js

W kolejnym kroku należy dodać AuthService w aplikacji Angular. auth.service.ts wygląda następująco:

@Injectable()
export class AuthService {

  userProfile: any;
  private userRoles: any;

  auth0 = new auth0.WebAuth({
    clientID: AUTH_CONFIG.clientID,
    domain: AUTH_CONFIG.domain,
    responseType: 'token id_token',
    audience: AUTH_CONFIG.audience,
    redirectUri: AUTH_CONFIG.callbackURL,
    scope: 'openid profile email'
  });

  constructor(private router: Router) {
    if (this.isAuthenticated())
      this.setProfileFromLocalStorage();
    else
      this.handleAuthentication();
  }

  public login(): void {
    this.auth0.authorize();
  }

  public handleAuthentication(): void {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        
        window.location.hash = '';
        this.setSession(authResult);
        this.setProfile((err, profile) => {
            this.userProfile = profile;
            this.userRoles = profile['http://api.tim-lab/roles'];
        });
        console.log(authResult);
        this.router.navigate(['/index']);
      } else if (err) {
        this.router.navigate(['/index']);
        console.log(err);
      }
    });
  }

  private setSession(authResult): void {
    const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
    localStorage.setItem('token', authResult.accessToken);
    localStorage.setItem('id_token', authResult.idToken);
    localStorage.setItem('expires_at', expiresAt);
  }

  public logout(): void {
    localStorage.removeItem('token');
    localStorage.removeItem('id_token');
    localStorage.removeItem('expires_at');
    this.userProfile = null;
    this.userRoles = null;
    this.router.navigate(['/']);
  }

  public isAuthenticated(): boolean {
    const expiresAt = JSON.parse(localStorage.getItem('expires_at') || '{}');
    return new Date().getTime() < expiresAt; } private setProfile(cb): void { const accessToken = localStorage.getItem('token'); if (!accessToken) { throw new Error('Access Token must exist to fetch profile'); } const self = this; this.auth0.client.userInfo(accessToken, (err, profile) => {
      if (profile) {
        self.userProfile = profile;
        self.userRoles = profile['http://api.tim-lab/roles'];
      }
      cb(err, profile);
    });
  }
  private setProfileFromLocalStorage() {
    this.userProfile = this.getUserData(this.getUserToken());
    if (this.userProfile)
      this.userRoles = this.userProfile['http://api.tim-lab/roles'];
  }

  private getUserToken() {
    return localStorage.getItem('id_token');
  }
  private getAuthToken() {
    return localStorage.getItem('token');
  }

  private getUserData(userToken) {
    var jwtHelper = new JwtHelper();
    var decodedToken = jwtHelper.decodeToken(userToken);
    return decodedToken;
  }

  public isInRole(roleName) {
    if (this.userRoles)
      return this.userRoles.indexOf(roleName) > -1;
  }

  public isInRoles(roles: string[]): boolean {
    if (!roles) return true;
      return this.userRoles && roles.every(r => this.userRoles.indexOf(r) >= 0);
  }

}

AuthService zawiera szereg funkcji odpowiedzialnych za obsługę użytkownika “na froncie”. Jest to między innymi metoda sprawdzająca czy user jest zalogowany, albo czy należy do określonej roli.

Wykorzystany został plik auth0-variables, zawierający konfigurację i klucz do API serwisu Auth0:

interface AuthConfig {
    audience: string;
    clientID: string;
    domain: string;
    callbackURL: string;
  }
  
  export const AUTH_CONFIG: AuthConfig = {
    clientID: '3t5oWTvM24JLOSiu52aWnldLisop9Ngo',
    domain: 'tim-lab.eu.auth0.com',
    callbackURL: 'http://localhost:4200/callback',
    audience: 'https://api.tim-lab.com'
  };

Jest to przykładowa konfiguracja z wykorzystaniem mojego konta Auth0 🙂

Poniżej znajduje się zawartość pliku auth-guard.ts, który pełni rolę przekierowania do strony logowania nieautoryzowanych użytkowników.

@Injectable()
export class AuthGuard implements CanActivate {
 constructor(private auth: AuthService, private router: Router) {}
 
  canActivate(route: ActivatedRouteSnapshot) {
    if (this.auth.isAuthenticated()) {
      if (this.auth.isInRoles(route.data.requiredRoles))
        return true;
      
      this.router.navigateByUrl(`https://tim-lab.eu.auth0.com/login?client=${AUTH_CONFIG.clientID}`);
      return false;
    }
    
    this.auth.login();
    return false;
  }
}

Fabryka authHttpServiceFactory.ts zawiera poniższy kod i pełni rolę obsługi żądań HTTPS:

export function authHttpServiceFactory(http: Http, options: RequestOptions) {
  return new AuthHttp(new AuthConfig({}), http, options);
}

Aby wszystko działało jak talala trzeba dodać do app.module.ts moduły i serwisy.

app.module.ts

Użycie mechanizmu logowania jest bardzo proste. W komponencie, w którym znajduje się przycisk logowania, wstrzykujemy AuthService w ten sposób:

auth-service

a następnie w widoku wywołujemy jego funkcję login():

login

można teraz uruchomić aplikację i kliknąć Log In, a naszym oczom ukaże się zaskakujący obrazek logowania:

log in screen

Outro

Każdy może zalogować się do systemu wykorzystując jedno z dwóch kont użytkownika: konto admina i konto usera, których login i hasło znajdują się w repozytorium https://github.com/MattKoboski/dotnetcorelovesangular

W części 2 pokażę, jak dodać role oraz obsłużyć komunikację z aplikacją .NET Core.

Do usłyszenia!