/
introspection-middleware.ts
135 lines (113 loc) · 3.89 KB
/
introspection-middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { SdkConfig } from '@cxcloud/ct-types/sdk';
import * as Cache from 'node-cache';
import 'isomorphic-fetch';
type IntrospectionResponse = {
tokenScopes: string;
expiresIn: number;
};
type Task = {
isFetching: boolean;
tasks: any[];
};
type TaskQueue = {
[id: string]: Task;
};
const mergeAuthHeader = (token: string, req: any) => ({
...req,
headers: {
...req.headers,
Authorization: `Bearer ${token}`
}
});
const calculateExpirationTime = (expiresIn: number) =>
Date.now() + expiresIn * 1000 - 1 * 60 * 60 * 1000; // Add a gap of 1 hour before expiration time.
export const createAuthMiddlewareForIntrospectionFlow = (
options: SdkConfig
) => {
let pendingTasks: TaskQueue = {};
const tokenCache = new Cache();
return (next: any) => async (request: any, response: any) => {
// If there's an Authorization header already, continue to next middleware
if (
(request.headers && request.headers.authorization) ||
(request.headers && request.headers.Authorization)
) {
return next(request, response);
}
const oauthTokenToValidate =
request.headers && request.headers['X-Custom-OAuth-Token'];
// If there's no custom oAuth token,
// we can ignore and go to next middleware
// so the next middleware can handle auth, like the client credentials flow
if (!oauthTokenToValidate) {
return next(request, response);
}
// Check if the the token is in cache
// if token exists and is valid, continue,
// otherwise invalidate the cache and fetch again.
const cached = tokenCache.get<IntrospectionResponse>(oauthTokenToValidate);
if (cached) {
if (Date.now() < cached.expiresIn) {
return next(mergeAuthHeader(oauthTokenToValidate, request), response);
}
// @TODO: handle refresh
return response.reject(new Error('Token has expired'));
}
// Queue all requests with the same OAuthToken
pendingTasks[oauthTokenToValidate] = pendingTasks[oauthTokenToValidate] || {
isFetching: false,
tasks: []
};
pendingTasks[oauthTokenToValidate].tasks.push({ request, response });
// Wait until the fetch is over
if (pendingTasks[oauthTokenToValidate].isFetching) {
return;
}
pendingTasks[oauthTokenToValidate].isFetching = true;
try {
const basicAuth = new Buffer(
`${options.admin.clientId}:${options.admin.clientSecret}`
).toString('base64');
// This refers to the Token Introspection endpoint
// http://dev.commercetools.com/http-api-authorization.html#oauth2-token-introspection
const body = `token=${oauthTokenToValidate}`;
const authResponse = await fetch(`${options.authHost}/oauth/introspect`, {
method: 'POST',
headers: {
Authorization: `Basic ${basicAuth}`,
'Content-Length': Buffer.byteLength(body).toString(),
'Content-Type': 'application/x-www-form-urlencoded'
},
body
});
if (!authResponse.ok) {
return; // handle error
}
const {
active: isTokenValid,
scope: tokenScopes,
exp: expiresIn
} = await authResponse.json();
if (!isTokenValid) {
console.log(isTokenValid, tokenScopes, expiresIn);
// @TODO: handle error here
return response.reject(new Error('Token is not valid'));
}
// Cache the response
tokenCache.set<IntrospectionResponse>(oauthTokenToValidate, {
tokenScopes,
expiresIn: calculateExpirationTime(expiresIn)
});
// Execute the queued up requests
const executionQueue = [...pendingTasks[oauthTokenToValidate].tasks];
// Reset the queue
delete pendingTasks[oauthTokenToValidate];
// Run tasks
executionQueue.forEach(task =>
next(mergeAuthHeader(oauthTokenToValidate, task.request), task.response)
);
} catch (err) {
response.reject(err);
}
};
};