adding authentication to angular appsapi security? sessions?cookies? tokens?! authentication basics...
TRANSCRIPT
@KimMaida
ADDING AUTHENTICATION TO ANGULAR APPS
@KimMaida
Hello! I’m Kim Maida
Manager, Community & Technical Content at Auth0 Angular Google Developer Expert
@KimMaida
“How do I secure my Angular app?”
@KimMaida
“How can I properly add authentication to my app to protect my data from unauthorized access?”
@KimMaida
API security? Sessions? Cookies? Tokens?
@KimMaida
API security? Sessions? Cookies? Tokens?
😵
AUTHENTICATION BASICS FOR WEB APPS
@KimMaida
Traditional web app authentication
@KimMaida
Authentication with Cookie-based Sessions
@KimMaida
Authentication with Cookie-based Sessions
@KimMaida
Authentication with Cookie-based Sessions
@KimMaida
Authentication with Cookie-based Sessions
@KimMaida
Authentication with Cookie-based Sessions
@KimMaida
Authentication with Cookie-based Sessions
</>HTML + Data
@KimMaida
Authentication with Cookie-based Sessions
@KimMaida
Authentication with Cookie-based Sessions
302HTML Error
@KimMaida
Authentication with Cookie-based Sessions
302
302HTML Error
?
@KimMaida
Authentication with Cookie-based Sessions
</>HTML + Data
@KimMaida
What about Single Page Apps?
@KimMaida
Single Page Application
@KimMaida
Single Page Application
@KimMaida
Single Page Application
@KimMaida
Single Page Application
@KimMaida
Single Page Application
{;}JSON
@KimMaida
Why not use cookie-based auth?
🤔
@KimMaida
Two Issues
✌
@KimMaida
Issue 1: APIs on Other Domains
my-domain.com
other-domain.com
X
@KimMaida
You (maybe): “No problem, everything is on one domain!”
$
@KimMaida
One more shortcoming
☝
@KimMaida
Issue 2: Authentication via Redirects & Ajax Calls
App Using Cookie-based Sessions
302
@KimMaida
Issue 2: Authentication via Redirects & Ajax Calls
Single Page ApplicationApp Using Cookie-based Sessions
302?
@KimMaida
Address these issues with token-based auth
@KimMaida
Single Page Application
@KimMaida
Single Page Application
@KimMaida
Single Page Application
#
@KimMaida
Single Page Application
@KimMaida
Single Page Application
#
@KimMaida
Don’t Store Sensitive Data in Local Storage
@KimMaida
Local Storage Security Concerns
! XSS can be used to steal all data in local storage
! XSS can be used to load malicious data into local storage
! Local Storage is shared within an origin
https://www.owasp.org/index.php/HTML5_Security_Cheat_Sheet#Local_Storage
@KimMaida
@KimMaida
Single Page Application
@KimMaida
Single Page Application
#
@KimMaida
Single Page Application
@KimMaida
Use Tokens to Call APIs
{;}JSON Data
@KimMaida
Use Tokens to Call APIs
my-domain.com
other-domain.com
@KimMaida
Solves the 2 Issues
1. APIs on other domains
2. Authentication via Redirects & Ajax Calls✅
✅
HOW DOES ANGULAR HELP?
@KimMaida
Angular Authentication TODO List
Manage authentication events
Attach token to API requests
Restrict access to certain routes
@KimMaida
Angular Authentication TODO List
Manage authentication events
Attach token to API requests
Restrict access to certain routes
@KimMaida
Angular Authentication TODO List
Manage authentication events
Send token with API requests
Restrict access to certain routes
@KimMaida
Angular Authentication TODO List
Manage authentication events
Send token with API requests
Restrict access to certain routes
@KimMaida
TODO: Manage authentication events
@KimMaida
@KimMaida
Create streams from callbacks
@KimMaida
Example: Bind Callbacks with RxJS
parseHash$ = bindNodeCallback( this.AuthSDK.parseHash .bind(this.Auth) );
parseHash$().subscribe( authData => this.setAuth(authData) );
@KimMaida
Example: Bind Callbacks with RxJS
parseHash$ = bindNodeCallback( this.AuthSDK.parseHash .bind(this.Auth) );
parseHash$().subscribe( authData => this.setAuth(authData) );
@KimMaida
Example: Bind Callbacks with RxJS
parseHash$ = bindNodeCallback( this.AuthSDK.parseHash .bind(this.Auth) );
parseHash$().subscribe( authData => this.setAuth(authData) );
@KimMaida
Example: Bind Callbacks with RxJS
parseHash$ = bindNodeCallback( this.AuthSDK.parseHash .bind(this.Auth) );
parseHash$().subscribe( authData => this.setAuth(authData) );
@KimMaida
Example: Bind Callbacks with RxJS
parseHash$ = bindNodeCallback( this.AuthSDK.parseHash .bind(this.Auth) );
parseHash$().subscribe( authData => this.setAuth(authData) );
@KimMaida
Create streams of auth data
@KimMaida
Create Auth Streams with RxJS
user$ = new BehaviorSubject<any>(null);
setAuth(authResult) { this.user$ .next(authResult.idTokenPayload); … }
@KimMaida
Create Auth Streams with RxJS
user$ = new BehaviorSubject<any>(null);
setAuth(authResult) { this.user$ .next(authResult.idTokenPayload); … }
@KimMaida
Create Auth Streams with RxJS
user$ = new BehaviorSubject<any>(null);
setAuth(authResult) { this.user$ .next(authResult.idTokenPayload); … }
@KimMaida
Create Auth Streams with RxJS
user$ = new BehaviorSubject<any>(null);
setAuth(authResult) { this.user$ .next(authResult.idTokenPayload); … }
@KimMaida
Use Auth Streams
<div *ngIf="(auth.user$ | async) as user"> <h1>{{ user.name }} </h1> </div>
@KimMaida
…and more
@KimMaida
Angular Authentication TODO List
Manage authentication events
Send token with API requests
Restrict access to certain routes
✅
@KimMaida
TODO: Send token with API requests
@KimMaida
HTTP Interceptors
@KimMaida
HTTP Interceptors
Interceptor Service{Authorization:`Bearer `}
@KimMaida
App Routing NgModule
@NgModule({ ...,
providers: [
AuthService,
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
], ...
@KimMaida
App Routing NgModule
@NgModule({ ...,
providers: [
AuthService,
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
], ...
@KimMaida
App Routing NgModule
@NgModule({ ...,
providers: [
AuthService,
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
], ...
@KimMaida
App Routing NgModule
@NgModule({ ...,
providers: [
AuthService,
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
], ...
@KimMaida
TokenInterceptor Service
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
const tokenReq = req.clone({
setHeaders: {Authorization: `Bearer ${this.auth.token}`}
});
return next.handle(tokenReq);
}
}
@KimMaida
TokenInterceptor Service
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
const tokenReq = req.clone({
setHeaders: {Authorization: `Bearer ${this.auth.token}`}
});
return next.handle(tokenReq);
}
}
@KimMaida
TokenInterceptor Service
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
const tokenReq = req.clone({
setHeaders: {Authorization: `Bearer ${this.auth.token}`}
});
return next.handle(tokenReq);
}
}
@KimMaida
TokenInterceptor Service
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
const tokenReq = req.clone({
setHeaders: {Authorization: `Bearer ${this.auth.token}`}
});
return next.handle(tokenReq);
}
}
@KimMaida
Conditional Interceptors
@KimMaida
Conditional Logic in Interceptors
intercept(req: HttpRequest<any>, next: HttpHandler) { if (req.url.indexOf('private') > -1) { const tokenReq = req.clone({ setHeaders: {Authorization: `Bearer ${this.auth.token}`}
});
return next.handle(tokenReq); } return next.handle(req);
}
@KimMaida
Conditional Logic in Interceptors
intercept(req: HttpRequest<any>, next: HttpHandler) { if (req.url.indexOf('private') > -1) { const tokenReq = req.clone({ setHeaders: {Authorization: `Bearer ${this.auth.token}`}
});
return next.handle(tokenReq); } return next.handle(req);
}
@KimMaida
Conditional Logic in Interceptors
intercept(req: HttpRequest<any>, next: HttpHandler) { if (req.url.indexOf('private') > -1) { const tokenReq = req.clone({ setHeaders: {Authorization: `Bearer ${this.auth.token}`}
});
return next.handle(tokenReq); } return next.handle(req);
}
@KimMaida
Conditional Logic in Interceptors
intercept(req: HttpRequest<any>, next: HttpHandler) { if (req.url.indexOf('private') > -1) { const tokenReq = req.clone({ setHeaders: {Authorization: `Bearer ${this.auth.token}`}
});
return next.handle(tokenReq); } return next.handle(req);
}
@KimMaida
Asynchronous Interceptors
@KimMaida
Async Interceptors
intercept(req: HttpRequest<any>, next: HttpHandler) { return this.auth.token$.pipe( mergeMap(token => { if (token) { const tokenReq = req.clone({ setHeaders: {Authorization:`Bearer ${token}`} }); return next.handle(tokenReq); } }) );} }
@KimMaida
Async Interceptors
intercept(req: HttpRequest<any>, next: HttpHandler) { return this.auth.token$.pipe( mergeMap(token => { if (token) { const tokenReq = req.clone({ setHeaders: {Authorization:`Bearer ${token}`} }); return next.handle(tokenReq); } }) );} }
@KimMaida
Async Interceptors
intercept(req: HttpRequest<any>, next: HttpHandler) { return this.auth.token$.pipe( mergeMap(token => { if (token) { const tokenReq = req.clone({ setHeaders: {Authorization:`Bearer ${token}`} }); return next.handle(tokenReq); } }) );} }
@KimMaida
Async Interceptors
intercept(req: HttpRequest<any>, next: HttpHandler) { return this.auth.token$.pipe( mergeMap(token => { if (token) { const tokenReq = req.clone({ setHeaders: {Authorization:`Bearer ${token}`} }); return next.handle(tokenReq); } }) );} }
@KimMaida
Angular Authentication TODO List
Manage authentication events
Send token with API requests
Restrict access to certain routes
✅
✅
@KimMaida
TODO: Restrict access to certain routes
@KimMaida
Route Guards
@KimMaida
Guard Interfaces - CanActivate
@KimMaida
Guard Interfaces - CanActivateChild
@KimMaida
Guard Interfaces - CanLoad
@KimMaida
Route Guards - CanActivate App Routes
import { AuthGuard } from './auth.guard';
export const ROUTES: Routes = [
{
path: 'profile',
component: ProfileComponent,
canActivate: [ AuthGuard ]
}
];
@KimMaida
Route Guards - CanActivate App Routes
import { AuthGuard } from './auth.guard';
export const ROUTES: Routes = [
{
path: 'profile',
component: ProfileComponent,
canActivate: [ AuthGuard ]
}
];
@KimMaida
Route Guards - CanActivate App Routes
import { AuthGuard } from './auth.guard';
export const ROUTES: Routes = [
{
path: 'profile',
component: ProfileComponent,
canActivate: [ AuthGuard ]
}
];
@KimMaida
Route Guards - CanActivate Service
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, public router: Router) {}
canActivate(): boolean {
if (!this.auth.isAuthenticated()) {
this.router.navigate(['login']);
return false;
}
return true;
}
}
@KimMaida
Route Guards - CanActivate Service
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, public router: Router) {}
canActivate(): boolean {
if (!this.auth.isAuthenticated()) {
this.router.navigate(['login']);
return false;
}
return true;
}
}
@KimMaida
Route Guards - CanActivate Service
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, public router: Router) {}
canActivate(): boolean {
if (!this.auth.isAuthenticated()) {
this.router.navigate(['login']);
return false;
}
return true;
}
}
@KimMaida
Route Guards - CanActivate Service
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, public router: Router) {}
canActivate(): boolean {
if (!this.auth.isAuthenticated()) {
this.router.navigate(['login']);
return false;
}
return true;
}
}
@KimMaida
Guarded Lazy Loading
@KimMaida
Lazy Loading with CanLoad Guard
export const ROUTES: Routes = [
{
path: 'admin',
loadChildren: './admin/admin.module#AdminModule',
canLoad: [ RoleGuard ],
data: { expectedRole: 'admin' }
}
];
@KimMaida
Lazy Loading with CanLoad Guard
export const ROUTES: Routes = [
{
path: 'admin',
loadChildren: './admin/admin.module#AdminModule',
canLoad: [ RoleGuard ],
data: { expectedRole: 'admin' }
}
];
@KimMaida
Lazy Loading with CanLoad Guard
export const ROUTES: Routes = [
{
path: 'admin',
loadChildren: './admin/admin.module#AdminModule',
canLoad: [ RoleGuard ],
data: { expectedRole: 'admin' }
}
];
@KimMaida
Lazy Loading with CanLoad Guard
export const ROUTES: Routes = [
{
path: 'admin',
loadChildren: './admin/admin.module#AdminModule',
canLoad: [ RoleGuard ],
data: { expectedRole: 'admin' }
}
];
@KimMaida
Route Guards - CanLoad
@Injectable()
export class RoleGuard implements CanLoad {
constructor(private auth: AuthService) {}
canLoad(route: Route): boolean {
if (!this.auth.isAuthenticated() ||
!this.auth.hasRole(route.data.expectedRole)) {
this.auth.login();
return false;
} …
@KimMaida
Route Guards - CanLoad
@Injectable()
export class RoleGuard implements CanLoad {
constructor(private auth: AuthService) {}
canLoad(route: Route): boolean {
if (!this.auth.isAuthenticated() ||
!this.auth.hasRole(route.data.expectedRole)) {
this.auth.login();
return false;
} …
@KimMaida
Route Guards - CanLoad
@Injectable()
export class RoleGuard implements CanLoad {
constructor(private auth: AuthService) {}
canLoad(route: Route): boolean {
if (!this.auth.isAuthenticated() ||
!this.auth.hasRole(route.data.expectedRole)) {
this.auth.login();
return false;
} …
@KimMaida
Angular Authentication TODO List
Manage authentication events
Send token with API requests
Restrict access to certain routes
✅
✅
✅
@KimMaida
@KimMaida
Adding Authentication to Angular Apps
bit.ly/angularmix-adding-auth
Thank you!