So you are working on an angular project & you have to use HTTP requests to communicate with back-end services. Angulars' HTTPClient is the go to choice in order to implement requests & it works amazing.
Then comes the part where you have to communicate with a secured resource, which usually means addition of authorization header to the request. One way is to add the header on all individual requests like below but it quickly becomes nuisance to add the header to many requests manually.
initGetUserData(): any {
// Get the token & create headers
const token = this.authService.GetAccessToken();
const headers = new HttpHeaders(
{ Authorization: `Bearer ${token}` }
);
this.httpClient.get('Secure_Url', { headers }).subscribe(response => {
});
}
We have a solution for reducing redundancy
This is where comes the usual choice of extending Angulars' request interceptor in which we can add any pre-processing logic such as addition of authorization header to our requests. It's a good practice to add any token refresh logic in the interceptor as well, so that users' experience is seamless & the original request can be completed once the token is refreshed
intercept(request: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
// Get token & add to request headers
let token = this.authService.GetAccessToken();
request = request.clone({
headers: request.headers
.set('Authorization', `Bearer ${token}`)
});
return next.handle(request).pipe(
catchError(err => {
if (err.status === 401) {
// Refresh tokens
return this.authService.InitRefresh().pipe(
switchMap((response) => {
// Get new token
token = this.authService.GetAccessToken();
request = request.clone({
headers: request.headers
.set('Authorization', `Bearer ${token}`)
});
// Continue original request
return next.handle(request);
})
);
}
}));
// Omitting error handling etc. for brevity
}
Behold, we have got everything setup so what's the pitch for?
All works fine and as expected until we have a component inside secured module that interacts with public API & not the secured resource. What usually happens is that the interceptor would try to intercept & add the authorization header to that request as well. Also the whole overhead of token refresh would be executed for public resources.
What's worse is that if user is not logged in & tries to access the component, which should work as it is a public view & should not require login, it would throw errors (if not handled) as the interceptor is trying to add/refresh token but there is no token available as user is not logged in.
But wait there is a way for tackling that as well
That's true, there is a solution for handling requests that we want to ignore, we can add a custom header to our requests or we can define an array of URLs' which should be omitted from interceptor authentication logic. Again, we soon reach to a point where it gets hard to keep track of all such out-of-way implementations
// Check for skip header
const isSkipHeader = request.headers.has('skip');
if (isSkipHeader) {
// Remove unnecessary header & proceed
request = request.clone({
headers: request.headers.delete('skip')
});
return next.handle(request);
}
Hence the proposed solution
We start off by creating a custom wrapper around Angulars' HTTP Client which would take care of following scenarios for us:
- Checking the token expiry BEFORE execution of call instead of after getting 401 response inside the interceptor , this would reduce overhead of 1 call (as original call is executed once instead of twice)
- Allowing us to override authentication with simple methods instead of custom headers
- Providing a central point to modify all requests (which interceptor does as well but is not very suitable for pre-processing as it requires use of operators in case of async methods)
- Providing ability to replace HTTP Client with any other 3rd party client without impacting other areas of application
- An easier way to customize & extend the requests based on requirements
We create a class with generic public methods for sending & retrieving data. We provide methods to override authentication, which would be very helpful for certain scenarios, we check for token expiry before the execution of call & proceed accordingly.
A lot of code have been omitted for brevity (Such as HTTP post, put, delete methods & error handling but it should be very easy to extend this
/**
* Interface for HTTP options
*/
export interface AppHttpOptions<T = any> {
Headers?: HttpHeaders;
Body?: T;
RequestUrl: string;
QueryParams?: object;
}
/**
* Application HTTP Client wrapper to provide authorization mechanism
* or any customization of requests
*/
@Injectable({
providedIn: 'root'
})
export class AppHttpClient {
// Pass this from environment variable
private baseUrl = 'baseUrl';
/**
* Constructor for client class, can be used to inject
* required resources
* @param httpClient Angular HTTP Client
*/
constructor(private httpClient: HttpClient,
private authService: AuthService) {
}
/**
* Initiates authorized Get request to the api
* @param httpOptions HttpOptions containing request data
*/
public GetAuthorized<ResponseType>(httpOptions: AppHttpOptions):
Promise<ResponseType> {
return this.getResponsePromise(httpOptions, 'post');
}
/**
* Initiates Get request to the api
* @param httpOptions HttpOptions containing request data
*/
public Get<ResponseType>(httpOptions: AppHttpOptions):
Promise<ResponseType> {
return this.getResponsePromise(httpOptions, 'get', false);
}
/**
* Creates a promise that resolves into HTTP response body
* @param httpOptions HttpOptions containing request data
* @param requestType Type of request i.e Get, Post, Put, Delete
*/
private getResponsePromise<ResponseType>
(httpOptions: AppHttpOptions,
requestType: 'post' | 'get' | 'delete' | 'put',
isAuth: boolean = true):
Promise<ResponseType> {
return new Promise((resolve, reject) => {
// Process the subscription & resolve the response
// i.e the request body response
this.getProcessedSubscription(httpOptions, requestType, isAuth).
then((response: ResponseType) => {
resolve(response);
}).catch(err => reject(err));
});
}
/**
* Subscribes to http request & returns the response as promise
* @param httpOptions HttpOptions containing request data
* @param requestType Type of request i.e Get, Post, Put, Delete
*/
private getProcessedSubscription<ResponseType>
(httpOptions: AppHttpOptions,
requestType: 'post' | 'get' | 'delete' | 'put',
isAuth: boolean):
Promise<ResponseType> {
return new Promise((resolve, reject) => {
this.getHttpRequest<ResponseType>
(httpOptions, requestType, isAuth).then(response => {
// Subscribe to HTTP request & resolve with the result
response.subscribe(result => {
resolve(result);
},
err => reject(err)
);
}).catch(err => reject(err));
});
}
/**
* Creates a promise to get the HTTP request observable
* @param httpOptions HttpOptions containing request data
* @param requestType Type of request i.e Get, Post, Put, Delete
*/
private getHttpRequest<ResponseType>
(httpOptions: AppHttpOptions,
requestType: 'post' | 'get' | 'delete' | 'put',
isAuth: boolean):
Promise<Observable<ResponseType>> {
return this.getAuthHeaders(httpOptions.Headers, isAuth).
then((headers: HttpHeaders) => {
// Append the query parameters
const options = this.addQueryParams(httpOptions);
// Create a HTTP request with angular HTTP Client
const request = this.httpClient.request<ResponseType>
(requestType,
this.baseUrl + options.RequestUrl,
{ body: options.Body, headers });
return request;
}).catch(err => Promise.reject(err));
}
/**
* Creates a promise that adds the authentication header
* to the request headers. Token retrieve & refresh logic can
* be easily handled as it is async operation
* @param headers Headers passed in with request
*/
private getAuthHeaders(headers: HttpHeaders, isAuth: boolean):
Promise<HttpHeaders> {
return new Promise((resolve) => {
// Only add authentication headers if required
if (isAuth) {
const token = this.authService.GetAccessToken();
if (headers) {
// Append authorization header
// * This is the core portions.
// We can apply all logics for checking token expiry,
// refreshing it & appending it to the headers
// without worrying about any side effects as we can
// resolve promise after all the other actions
headers.append('Authorization', `Bearer ${token}`);
}
else {
// Create new headers object if not passed in
headers = new HttpHeaders({
Authorization: `Bearer ${token}`
});
}
}
resolve(headers);
});
}
/**
* @param httpOptions HttpOptions containing request data
* @param httpOptions Add
*/
private addQueryParams(httpOptions: AppHttpOptions): AppHttpOptions {
if (httpOptions.QueryParams) {
// Create the parameters string from the provided parameters
const query = Object.keys(httpOptions.QueryParams)
.map(k => k + '=' + httpOptions.QueryParams[k])
.join('&');
// Append the parameters to the request URL
httpOptions.RequestUrl = `${httpOptions.RequestUrl}?${query}`;
}
return httpOptions;
}
}
And we are done! In order to use the methods, we simply inject our class & call the appropriate methods with minimal configs
constructor(private httpClient: AppHttpClient) { }
initGetData(): any {
// Public resource request
this.httpClient.Get({ RequestUrl: 'Public_Url'}).
then(response => {
});
// Secured resource request
this.httpClient.GetAuthorized({ RequestUrl: 'Secure_Url' }).
then(response => {
});
}
Implementation of above can be modified with number of options based on use cases e.g. checking token expiry & refreshing before initiating call, passing custom headers with specific requests without much hassle etc.
Let me know what do you guys use to handle such scenarios or any other alternatives that might be more impactful.
Happy coding !
Top comments (0)