Skip to content
This repository was archived by the owner on Dec 8, 2022. It is now read-only.

Commit c46e0fb

Browse files
Created auth interceptor for use with Angular's HttpClient (#11)
* Created auth interceptor for use with Angular's HttpClient The implementation contains a workaround for passing additional context to the interceptor since Angualr doesn't currently support it. Here is an issue describing the problem: angular/angular#18155 * Formatted imports/exports * Removed unused parameter * Added comment to code coverage suppression * Created `SkyAuthHttpClientModule`
1 parent 382e0f1 commit c46e0fb

9 files changed

+420
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//#region imports
2+
3+
import {
4+
NgModule
5+
} from '@angular/core';
6+
7+
import {
8+
HTTP_INTERCEPTORS,
9+
HttpClientModule
10+
} from '@angular/common/http';
11+
12+
import {
13+
SkyAuthInterceptor
14+
} from './auth-interceptor';
15+
16+
//#endregion
17+
18+
@NgModule({
19+
providers: [
20+
{
21+
provide: HTTP_INTERCEPTORS,
22+
useClass: SkyAuthInterceptor,
23+
multi: true
24+
}
25+
],
26+
exports: [
27+
HttpClientModule
28+
]
29+
})
30+
export class SkyAuthHttpClientModule { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const SKY_AUTH_PARAM_AUTH = 'sky_auth';
2+
export const SKY_AUTH_PARAM_PERMISSION_SCOPE = 'sky_permissionScope';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//#region imports
2+
3+
import {
4+
HttpEventType,
5+
HttpHandler,
6+
HttpParams,
7+
HttpRequest,
8+
HttpSentEvent
9+
} from '@angular/common/http';
10+
11+
import {
12+
Observable
13+
} from 'rxjs/Observable';
14+
15+
import 'rxjs/add/observable/of';
16+
17+
import {
18+
SkyAuthInterceptor
19+
} from './auth-interceptor';
20+
21+
import {
22+
SKY_AUTH_PARAM_AUTH,
23+
SKY_AUTH_PARAM_PERMISSION_SCOPE
24+
} from './auth-interceptor-params';
25+
26+
//#endregion
27+
28+
class MockHttpHandler extends HttpHandler {
29+
public handle() {
30+
return Observable.of({
31+
type: HttpEventType.Sent
32+
} as HttpSentEvent);
33+
}
34+
}
35+
36+
describe('Auth interceptor', () => {
37+
let mockTokenProvider = {
38+
getToken: () => Promise.resolve('abc')
39+
};
40+
41+
function createInteceptor(envId?: string, leId?: string, url?: string) {
42+
return new SkyAuthInterceptor(
43+
mockTokenProvider,
44+
{
45+
runtime: {
46+
params: {
47+
get: (name: string) => {
48+
switch (name) {
49+
case 'envid':
50+
return envId;
51+
case 'leid':
52+
return leId;
53+
default:
54+
return undefined;
55+
}
56+
},
57+
getUrl: () => url || 'https://example.com/get/'
58+
}
59+
} as any,
60+
skyux: {}
61+
});
62+
}
63+
64+
function createRequest(params?: HttpParams) {
65+
const request = new HttpRequest(
66+
'GET',
67+
'https://example.com/get/',
68+
{
69+
params: params
70+
}
71+
);
72+
73+
return request;
74+
}
75+
76+
function validateAuthRequest(
77+
next: MockHttpHandler,
78+
done: DoneFn,
79+
cb: (authRequest: HttpRequest<any>) => void
80+
) {
81+
spyOn(next, 'handle').and.callFake(
82+
(authRequest: HttpRequest<any>) => {
83+
cb(authRequest);
84+
done();
85+
return Observable.of('');
86+
}
87+
);
88+
}
89+
90+
function validateContext(
91+
envId: string,
92+
leId: string,
93+
permissionScope: string,
94+
expectedUrl: string,
95+
done: DoneFn
96+
) {
97+
const interceptor = createInteceptor(envId, leId, expectedUrl);
98+
99+
let params = new HttpParams().set(SKY_AUTH_PARAM_AUTH, 'true');
100+
101+
if (permissionScope) {
102+
params = params.set(SKY_AUTH_PARAM_PERMISSION_SCOPE, permissionScope);
103+
}
104+
105+
const request = createRequest(params);
106+
107+
const next = new MockHttpHandler();
108+
109+
const getTokenSpy = spyOn(mockTokenProvider, 'getToken').and.callThrough();
110+
111+
interceptor.intercept(request, next);
112+
113+
validateAuthRequest(next, done, (authRequest) => {
114+
expect(authRequest.url).toBe(expectedUrl);
115+
});
116+
117+
interceptor.intercept(request, next).subscribe(() => {});
118+
119+
const expectedTokenArgs: any = {};
120+
121+
if (envId) {
122+
expectedTokenArgs.envId = envId;
123+
}
124+
125+
if (leId) {
126+
expectedTokenArgs.leId = leId;
127+
}
128+
129+
expect(getTokenSpy).toHaveBeenCalledWith(
130+
jasmine.objectContaining(expectedTokenArgs)
131+
);
132+
}
133+
134+
it('should pass through the existing request when not an auth request', () => {
135+
const interceptor = createInteceptor();
136+
137+
const request = createRequest();
138+
139+
const next = new MockHttpHandler();
140+
141+
const handleSpy = spyOn(next, 'handle');
142+
143+
interceptor.intercept(request, next);
144+
145+
expect(handleSpy).toHaveBeenCalledWith(request);
146+
});
147+
148+
it('should add a token to the request if the sky_auth parameter is set', (done) => {
149+
const interceptor = createInteceptor();
150+
151+
const request = createRequest(
152+
new HttpParams()
153+
.set(SKY_AUTH_PARAM_AUTH, 'true')
154+
);
155+
156+
const next = new MockHttpHandler();
157+
158+
validateAuthRequest(next, done, (authRequest) => {
159+
expect(authRequest.headers.get('Authorization')).toBe('Bearer abc');
160+
});
161+
162+
interceptor.intercept(request, next).subscribe(() => {});
163+
});
164+
165+
it('should add a permission scope to the token request if specified', (done) => {
166+
validateContext(undefined, undefined, '123', 'https://example.com/get/', done);
167+
});
168+
169+
it('should apply the appropriate environment context', (done) => {
170+
validateContext('abc', undefined, undefined, 'https://example.com/get/?envid=abc', done);
171+
});
172+
173+
it('should apply the appropriate legal entity context', (done) => {
174+
validateContext(undefined, 'abc', undefined, 'https://example.com/get/?leid=abc', done);
175+
});
176+
177+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//#region imports
2+
3+
import {
4+
Injectable
5+
} from '@angular/core';
6+
7+
import {
8+
HttpEvent,
9+
HttpHandler,
10+
HttpInterceptor,
11+
HttpRequest
12+
} from '@angular/common/http';
13+
14+
import {
15+
Observable
16+
} from 'rxjs/Observable';
17+
18+
import 'rxjs/add/observable/fromPromise';
19+
import 'rxjs/add/operator/switchMap';
20+
21+
import {
22+
SkyAppConfig
23+
} from '@skyux/config';
24+
25+
import {
26+
SkyAuthTokenProvider
27+
} from '../auth-http/auth-token-provider';
28+
29+
import {
30+
SKY_AUTH_PARAM_AUTH,
31+
SKY_AUTH_PARAM_PERMISSION_SCOPE
32+
} from './auth-interceptor-params';
33+
34+
//#endregion
35+
36+
function removeSkyParams(request: HttpRequest<any>): HttpRequest<any> {
37+
// The if statement here is just a sanity check; it appears that by the time
38+
// this interceptor is called, the params property is always defined, even if
39+
// it's not provided when the HTTP request is created.
40+
/* istanbul ignore else */
41+
if (request.params) {
42+
request = request.clone(
43+
{
44+
params: request.params
45+
.delete(SKY_AUTH_PARAM_AUTH)
46+
.delete(SKY_AUTH_PARAM_PERMISSION_SCOPE)
47+
}
48+
);
49+
}
50+
51+
return request;
52+
}
53+
54+
@Injectable()
55+
export class SkyAuthInterceptor implements HttpInterceptor {
56+
constructor(
57+
private tokenProvider: SkyAuthTokenProvider,
58+
private config: SkyAppConfig
59+
) { }
60+
61+
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
62+
let auth: boolean;
63+
let permissionScope: string;
64+
65+
const params = request.params;
66+
67+
if (
68+
params &&
69+
(
70+
params.has(SKY_AUTH_PARAM_AUTH) ||
71+
params.has(SKY_AUTH_PARAM_PERMISSION_SCOPE)
72+
)
73+
) {
74+
auth = params.get(SKY_AUTH_PARAM_AUTH) === 'true';
75+
permissionScope = params.get(SKY_AUTH_PARAM_PERMISSION_SCOPE);
76+
77+
request = removeSkyParams(request);
78+
}
79+
80+
if (auth) {
81+
const leId = this.getLeId();
82+
const envId = this.getEnvId();
83+
const tokenArgs: any = {};
84+
85+
if (permissionScope) {
86+
tokenArgs.permissionScope = permissionScope;
87+
}
88+
89+
if (envId) {
90+
tokenArgs.envId = envId;
91+
}
92+
93+
if (leId) {
94+
tokenArgs.leId = leId;
95+
}
96+
97+
return Observable
98+
.fromPromise(this.tokenProvider.getToken(tokenArgs))
99+
.switchMap((token) => {
100+
let authRequest = request.clone({
101+
setHeaders: {
102+
Authorization: `Bearer ${token}`
103+
},
104+
url: this.config.runtime.params.getUrl(request.url)
105+
});
106+
107+
return next.handle(authRequest);
108+
});
109+
}
110+
111+
return next.handle(request);
112+
}
113+
114+
private getEnvId(): string {
115+
return this.config.runtime.params.get('envid');
116+
}
117+
118+
private getLeId(): string {
119+
return this.config.runtime.params.get('leid');
120+
}
121+
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//#region imports
2+
3+
import {
4+
HttpParams
5+
} from '@angular/common/http';
6+
7+
import {
8+
skyAuthHttpOptions
9+
} from './auth-options';
10+
11+
//#endregion
12+
13+
describe('Auth options', () => {
14+
15+
it('should add an auth parameter to the resulting options object', () => {
16+
const options = skyAuthHttpOptions();
17+
18+
expect(options.params.get('sky_auth')).toBe('true');
19+
});
20+
21+
it('should add a permission scope parameter and remove it from the options', () => {
22+
const options = skyAuthHttpOptions({
23+
permissionScope: 'abc'
24+
});
25+
26+
expect(options.params.get('sky_permissionScope')).toBe('abc');
27+
28+
expect('permissionScope' in options).toBe(false);
29+
});
30+
31+
it('should preserve existing parameters', () => {
32+
const options = skyAuthHttpOptions({
33+
params: new HttpParams()
34+
.set('abc', '123')
35+
});
36+
37+
expect(options.params.get('abc')).toBe('123');
38+
});
39+
40+
});

0 commit comments

Comments
 (0)