Author: Bibhas Bhattacharya
This tutorial is adapted from the Web Age course Advanced Angular 12 Programming Training.
Authentication is the process of reliably identifying a user.
Authorization is the process of giving a user access to only the resources she is allowed to access.
Common programming tasks:
- Provide a login page where user enters her credentials.
- Upon a successful login the server issues a token identifying the user. The browser side saves that token in memory or local storage.
- Limit access to application functions based on user privilege. This is usually done using role based security.
- With every HTTP call to the backend include the user identity token. Depending on the technology used this is usually done as a cookie or HTTP header.
- If the user accesses a protected resource (such as an Angular route) and the identity token does not exist or has expired then redirect to the login page.
- If a HTTP call returns 403 or similar then redirect to the login page.
The two most common ways to create an identity token are SAML2 and JWT.
In the end, they both achieve the same goals:
- An identity token issued by the server contains user’s unique ID, name, e-mail address, various security roles, and any other kind of business-specific payload.
- The token is signed by the server. It is very difficult to generate a fake token by a third party. When a client sends a token with an HTTP request the server can reliably ensure that the token was issued by it and has not been tampered with.
- A token is however vulnerable to stealing via man in a middle attack. The server can not discern if a token was stolen. To prevent that always transfer tokens over HTTPS and set a token to expire every few hours.
SAML2 is widely used in the enterprise. But JWT is a more modern standard and much easier to use. In this chapter, we will use JWT in the examples but the basic approach will be the same for SAML2.
Upon successful login, the server will issue the identity token. The client needs to send this back in the future with every HTTP request. Exactly how the server and client do this depends on various things. Here are two common scenarios:
- Cookie-based – The server expects the token to be in a cookie. In which case the server sets the cookie and the browser automatically sends it back with every request.
- Header based – In JWT the client usually sends the token back in the “Authorization” request header. In that case, the server issues the token in the response body of the login request. The client needs to write code to save that token and set the Authorization header for all future requests.
//Example response from server containing a JWT token
{
token: "abc-123"
}
//Example Authorization request header
Authorization: Bearer abc-123
Saves identity token in session storage
@Injectable(...)
export class AuthenticationService {
constructor(private http:HttpClient) { }
login(credentials:Object) : Observable<Object> {
return this.http.post<Object>("/login", credentials)
.pipe(tap(response =>
window.sessionStorage.setItem("auth-token",
response)))
}
getAuthToken() : string {
return window.sessionStorage.getItem("auth-token")
}
}
The example above assumes that the identity token is available from the response of the “/login” call as follows:
{
token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.0gyt4I1ZFGzzDZBrK9jwOIH4NSWvs0k4snp-ZcwKLVU"
}
You can save the token in a service class member variable. Because Angular apps are single page the service instance is preserved as the user navigates between views.
In session storage: This is per server domain and per tab. The storage is cleared when the tab is closed.
In local storage. This is per domain and survives a browser restart.
You can develop an interceptor to set the Authorization header for every backend call.
@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
constructor(private authSvc: AuthenticationService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.authSvc.getAuthToken() === null) {
return next.handle(req)
}
req = req.clone({
setHeaders: { "Authorization": `Bearer ${this.authSvc.getAuthToken()}` }
})
return next.handle(req)
}
}
JWT allows the server to encode user information such as full name, profile photo URL and user roles in the identity token. This information is called payload of the token.
You can decode the token to obtain the payload. Use the jwt-decode package to do this.
npm install jwt-decode
npm install @types/jwt-decode
Example code.
import jwt_decode from "jwt-decode";
export class UserProfile {
photoURL!:string
name!:string
roles!:string[]
}
let jwtToken = "ABC1728...2xcv"
let userProfile = jwt_decode<UserProfile>(jwtToken)
This attack is mounted by a user by somehow embedding a <script> tag in a page. This can be done for example by submitting a comment or writing a user profile that contains the <script> tag.
When another user visits the infected page, the script can then collect private information about that user from the DOM such as cookie, names, and emails. This information then can be passed to a third-party site using a mechanism that bypasses the same-origin policy. Such as setting the src attribute of an image or adding a <script> tag with a source from another site.
XSS is one of the most common forms of a security breach on the web.
To prevent XSS reject any HTML content from untrusted sources. For example, you can severely limit or completely eliminate any HTML in user comments. But allow company staff to allow entering HTML in product catalog content.
By default, Angular considers values stored in any component variable as untrusted. But any HTML directly entered in the template is trusted.
In this example, the <div> and <b> tags in the template are trusted. But the <h1> tag in the comment variable is not trusted. Angular will sanitize variables (escape the HTML) before setting DOM element properties.
@Component({...
template: `<div><b>Comment:</b> {{comment}}</div>`
})
export class AppComponent {
comment = '<h1>Hello World</h1>'
}
//Generates this DOM element
<div><b>Comment:</b> <h1>Hello World</h1></div>
Angular uses different strategies to sanitize the value in a variable depending on where in the template the variable is used:
- HTML – When the variable is used to set the innerHTML of an element. We see this in the example in the previous slide.
- Style – When you bind the variable to the “style” property of an element.
- URL – When you bind the variable to the “href” property.
- Resource URL – When you bind the variable to the “src” attribute of an image or script.
A different strategy is used in these cases because an XSS attack is mounted differently in each of these situations.
To bypass Angular’s sanitization for trusted content take these steps:
- Convert a string to SafeHtml by calling the bypassSecurityTrustHtml() method of DomSanitizer service.
- Bind the SafeHtml object to the innerHTML property of an element.
The same example as before converted to bypass sanitization.
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({...,
template: `<div><b>Comment:</b> <span ="getSafeComment()"></span></div>`
})
export class AppComponent {
comment = '<h1>Hello World</h1>'
constructor(private sanitizer: DomSanitizer){}
getSafeComment() : SafeHtml {
return this
.sanitizer
.bypassSecurityTrustHtml(this.comment)
}
}
In this attack, a page from a malicious site (evil.com) somehow sends a request to a legitimate site (good.com). Such a request needs to bypass the same-origin policy which is quite easy. For example, request an image from good.com or submit a form using JavaScript.
If the user is currently logged into the legitimate site then such a request can cause all kinds of unintended consequence. The damage level increases with the level of privilege the user has with the legitimate site.
XSRF attack is technically very easy to mount. The only challenge is in how to lure users into the malicious site. This is usually done through phishing attack.
Details of XSRF and strategies to mitigate it are beyond the scope of this class. Read about it in www.owasp.org.
The server issues a difficult-to-guess unique token. The Angular app sends the token back with each request in a custom request header. The server verifies the token. Currently there is no way for a malicious web page to set custom headers prior to requesting an image or submitting a form to the legitimate web site.
Angular’s HttpClient service has some support for this. To use this, take these steps:
- The server must first issue a unique token using the XSRF-TOKEN cookie. set-cookie: XSRF-TOKEN=example-xsrf-token;Path=/
- For all future POST, PUT, DELETE requests HttpClient will set the X-XSRF-TOKEN custom header. X-XSRF-TOKEN: example-xsrf-token
- From the server-side verify that X-XSRF-TOKEN custom header is set. Frequently issue a new token to further strengthen security.
- In addition, take all other mitigation steps outlined in OWASP web site. Like verify the Referer request header to make sure request is coming from a page served by the legitimate web site.
In this tutorial, we covered:
- Upon successful authentication, the server should issue an identity token. Angular app should save the token in memory, local or session storage depending on the business requirement.
- To make any secure backend call the application needs to send that token back. Server validates the token to make sure that it was not made up by a third party or has not expired.
- You can obtain user information such as name and email by decoding the token.
- Angular provides ways to mitigate Cross Site Scripting and Cross Site Request Forgery attacks.