Skip to content

Commit 6e7f3a4

Browse files
Memcached test implementation complete
1 parent c157f83 commit 6e7f3a4

11 files changed

Lines changed: 345 additions & 234 deletions

File tree

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@
3737
"jsonwebtoken": "^8.5.1",
3838
"jwks-rsa": "^1.12.0",
3939
"jwt-decode": "^3.1.2",
40-
"memcached-mock": "^0.1.0",
41-
"node-fetch": "^2.6.1"
40+
"memcached-node": "^0.1.0",
41+
"node-fetch": "^2.6.1",
42+
"typemoq": "^2.1.0"
4243
},
4344
"devDependencies": {
4445
"@types/jest": "^26.0.15",
4546
"@types/node": "^12.12.67",
4647
"baretest": "^2.0.0",
4748
"jest": "^26.6.3",
49+
"memcached-mock": "^0.1.0",
4850
"ts-jest": "^26.4.4",
4951
"typescript": "^4.0.3"
5052
}

src/auth0.ts

Lines changed: 36 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,190 +1,52 @@
1-
import { FastifyRequest} from "fastify";
2-
import fetch from "node-fetch";
1+
import {getUserById, getUserIdFromAuth0} from "./dependencies/auth0Api";
2+
import {Memcached, ResponseCode} from "memcached-node";
33

4-
let moduleAdminToken: string;
5-
6-
async function getAdminToken(refresh : boolean) {
7-
if(refresh || !moduleAdminToken) {
8-
let mgmtDomain = process.env.AUTH0_DOMAIN;
9-
10-
let refreshResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, {
11-
"method": "post",
12-
"headers": {
13-
"Content-Type": "application/json"
14-
},
15-
body: JSON.stringify({
16-
"grant_type": "client_credentials",
17-
"client_id": process.env.AUTH0_MGMT_CLIENT_ID,
18-
"client_secret": process.env.AUTH0_MGMT_CLIENT_SECRET,
19-
"audience": `https://${mgmtDomain}/api/v2/`,
20-
"scope": "read:users update:users read:users_app_metadata update:users_app_metadata"
21-
})
22-
});
23-
let refreshJson = await refreshResponse.json();
24-
moduleAdminToken = refreshJson.access_token
25-
}
26-
return moduleAdminToken;
27-
}
28-
29-
export interface Auth0UserInfo {
30-
user_id: string, // Should be URL encoded since it may contain characters that do not work well in a URL.
31-
username?: string,
32-
email?: string
33-
email_verified?: boolean,
34-
phone_number?: string
35-
phone_verified?: boolean,
36-
created_at?: string,
37-
updated_at?: string,
38-
identities?: [
39-
{
40-
connection?: string,
41-
user_id: string,
42-
provider: string,
43-
isSocial?: boolean
44-
}
45-
],
46-
app_metadata?: any,
47-
user_metadata?: any,
48-
picture?: string,
49-
name?: string,
50-
nickname?: string,
51-
multifactor?: string[],
52-
last_ip?: string,
53-
last_login?: string,
54-
logins_count?: number,
55-
blocked?: boolean,
56-
given_name?: string,
57-
family_name?: string
58-
}
4+
export const LIFETIME_SECONDS = 60 * 30; // half an hour
5+
export type MinimalRequest = { headers?: {authorization?: string }};
6+
export type Auth0JwtVerifier = (request: MinimalRequest) => Promise<UserAuthEntry>;
7+
export type UserAuthEntry = {userAppId: string, admin: boolean, role: string};
598

60-
export interface UserInfoPatch {
61-
blocked?: boolean,
62-
email_verified?: boolean,
63-
email?: string,
64-
phone_number?: string,
65-
phone_verified?: boolean,
66-
user_metadata?: any,
67-
app_metadata?: any,
68-
given_name?: string,
69-
family_name?: string,
70-
name?: string,
71-
nickname?: string,
72-
picture?: string,
73-
verify_email?: boolean,
74-
verify_phone_number?: boolean,
75-
password?: string,
76-
connection?: string,
77-
client_id?: string,
78-
username?: string
9+
export function getJwtVerifier(cache: Memcached, getUserInfo = getUserIdFromAuth0, getUserRole = getUserRoleFromAuth0 ) : Auth0JwtVerifier {
10+
return (request : MinimalRequest) => verifyJwt(request, cache, getUserInfo, getUserRole);
7911
}
8012

81-
type UserRole = {role: string};
82-
async function getUserRole(userId: string) : Promise<UserRole> {
13+
async function getUserRoleFromAuth0(userId: string) : Promise<string> {
8314
let app_metadata = (await getUserById(userId)).app_metadata;
84-
return {role: app_metadata.role};
85-
}
86-
87-
/*
88-
Get user by ID: https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id
89-
Get all users: https://auth0.com/docs/api/management/v2#!/Users/get_users
90-
Update a user: https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id
91-
*/
92-
93-
export async function getUserById(userId: string) : Promise<Auth0UserInfo> {
94-
userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`;
95-
return callManagementApi<Auth0UserInfo>(`/users/${userId}`);
96-
}
97-
98-
export async function getAllUsers(per_page? : number, page?: number) {
99-
let querystring = "";
100-
if(!!per_page || !!page) {
101-
querystring = "?";
102-
let perPageClause = !per_page ? "" : `per_page=${per_page}`
103-
let pageClause = !page ? "" : `page=${page}`;
104-
querystring = `?${perPageClause}&${pageClause}`;
105-
}
106-
return callManagementApi<Auth0UserInfo[]>(`/users${querystring}`);
15+
return app_metadata.role;
10716
}
10817

109-
export async function updateUser(userId: string, userInfoPatch: UserInfoPatch) {
110-
userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`;
111-
console.log("Patching from API");
112-
return callManagementApi<Auth0UserInfo>(`/users/${userId}`, "PATCH", JSON.stringify(userInfoPatch));
113-
}
114-
115-
async function callManagementApi<T>(path: string, method = "GET", body = "", refresh = false) : Promise<T> {
116-
let mgmtDomain = process.env.AUTH0_DOMAIN;
117-
let adminToken = await getAdminToken(refresh);
118-
let url = encodeURI(`https://${mgmtDomain}/api/v2${path}`);
119-
let options : any = {
120-
method,
121-
"headers": {
122-
"Authorization": `Bearer ${adminToken}`
123-
}
124-
};
125-
if(!!body) {
126-
options.headers["Content-Type"] = "application/json";
127-
options.body = body;
128-
}
129-
let metaResponse = await fetch(url, options);
130-
if(metaResponse.status === 200) {
131-
return await metaResponse.json();
132-
} else if (!refresh && metaResponse.status === 401) {
133-
return await callManagementApi<T>(path, method, body,true);
134-
} else {
135-
throw new Error(JSON.stringify(metaResponse));
136-
}
137-
}
138-
139-
export type UserID = {userId: string};
140-
export async function getUserInfo(authHeader: string) : Promise<UserID> {
141-
try {
142-
let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, {
143-
"method": "GET",
144-
"headers": {"Authorization": authHeader}
145-
});
146-
let payload: any = await userResponse.json();
147-
let userId : string = !payload.sub ? "" : payload.sub;
148-
return {userId};
149-
} catch (e) {
150-
throw e;
151-
}
152-
}
153-
export type UserAuthEntry = {userAppId: string, admin: boolean, role: string};
154-
export type Auth0JwtVerifier = (request: FastifyRequest) => Promise<UserAuthEntry>;
155-
export type ICacheEntry = {
156-
timestamp: number,
157-
data: UserAuthEntry
158-
};
159-
160-
let cache = new Map<string, ICacheEntry>();
161-
const LIFETIME_MILLISECONDS = 1000 * 60 * 30; // half an hour
162-
export async function verifyJwtCached(authHeader: string, cache: Map<string, ICacheEntry>, getUserInfo: (token: string) => Promise<UserID>, getUserRole: (id: string) => Promise<UserRole>) {
18+
async function verifyJwtCached(authHeader: string, cache: Memcached) {
16319
if(!authHeader) {
164-
return {userAppId: "", admin: false, role: ""};
20+
return null;
16521
} else {
166-
let cachedAuth = cache.get(authHeader);
167-
if(!cachedAuth || Date.now() > cachedAuth.timestamp + LIFETIME_MILLISECONDS) {
168-
let {userId} = await getUserInfo(authHeader);
169-
let {role} = await getUserRole(userId);
170-
let admin: boolean = role === "admin"
171-
let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId;
172-
cachedAuth = {timestamp: Date.now(), data: {userAppId, admin, role}};
173-
cache.set(authHeader, cachedAuth);
22+
let cachedData = await cache.get(authHeader);
23+
if (cachedData.code === ResponseCode.EXISTS && !!cachedData.data) {
24+
let headerMetadata = cachedData.data[authHeader];
25+
if (!!headerMetadata && !!headerMetadata.value) {
26+
return JSON.parse(headerMetadata.value.toString());
27+
} else {
28+
return null;
29+
}
30+
} else {
31+
return null;
17432
}
175-
return cachedAuth.data;
17633
}
17734
}
17835

179-
export async function verifyJwt(request: FastifyRequest) {
180-
let authHeader = !request.headers.authorization ? "" : request.headers.authorization;
181-
return await verifyJwtCached(authHeader, cache, getUserInfo, getUserRole);
36+
async function verifyJwtFromAuth0(authHeader: string, getUserInfo: (token: string) => Promise<string>, getUserRole: (id: string) => Promise<string>) {
37+
let userId = await getUserInfo(authHeader);
38+
let role = await getUserRole(userId);
39+
let admin: boolean = role === "admin"
40+
let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId;
41+
return {userAppId, admin, role};
18242
}
18343

184-
export function removeUserFromCache(request: FastifyRequest) {
185-
let authHeader = !request.headers.authorization ? "" : request.headers.authorization;
186-
deleteCachedUser(authHeader);
187-
}
188-
export function deleteCachedUser(authHeader: string) {
189-
cache.delete(authHeader);
44+
async function verifyJwt(request: MinimalRequest, userCache: Memcached, getUserInfo: (token: string) => Promise<string>, getUserRole: (userId: string) => Promise<string>) {
45+
let authHeader = !!request.headers && !!request.headers.authorization ? request.headers.authorization : "";
46+
let userData = await verifyJwtCached(authHeader, userCache);
47+
if(!userData) {
48+
userData = await verifyJwtFromAuth0(authHeader, getUserInfo, getUserRole)
49+
await userCache.add(authHeader, userData, {expires: LIFETIME_SECONDS});
50+
}
51+
return userData;
19052
}

src/dependencies/auth0Api.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import fetch from "node-fetch";
2+
3+
let moduleAdminToken: string;
4+
5+
export interface Auth0UserInfo {
6+
user_id: string, // Should be URL encoded since it may contain characters that do not work well in a URL.
7+
username?: string,
8+
email?: string
9+
email_verified?: boolean,
10+
phone_number?: string
11+
phone_verified?: boolean,
12+
created_at?: string,
13+
updated_at?: string,
14+
identities?: [
15+
{
16+
connection?: string,
17+
user_id: string,
18+
provider: string,
19+
isSocial?: boolean
20+
}
21+
],
22+
app_metadata?: any,
23+
user_metadata?: any,
24+
picture?: string,
25+
name?: string,
26+
nickname?: string,
27+
multifactor?: string[],
28+
last_ip?: string,
29+
last_login?: string,
30+
logins_count?: number,
31+
blocked?: boolean,
32+
given_name?: string,
33+
family_name?: string
34+
}
35+
36+
export interface UserInfoPatch {
37+
blocked?: boolean,
38+
email_verified?: boolean,
39+
email?: string,
40+
phone_number?: string,
41+
phone_verified?: boolean,
42+
user_metadata?: any,
43+
app_metadata?: any,
44+
given_name?: string,
45+
family_name?: string,
46+
name?: string,
47+
nickname?: string,
48+
picture?: string,
49+
verify_email?: boolean,
50+
verify_phone_number?: boolean,
51+
password?: string,
52+
connection?: string,
53+
client_id?: string,
54+
username?: string
55+
}
56+
57+
/*
58+
Get user by ID: https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id
59+
Get all users: https://auth0.com/docs/api/management/v2#!/Users/get_users
60+
Update a user: https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id
61+
*/
62+
export async function getUserById(userId: string) : Promise<Auth0UserInfo> {
63+
userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`;
64+
return callManagementApi<Auth0UserInfo>(`/users/${userId}`);
65+
}
66+
67+
export async function getAllUsers(per_page? : number, page?: number) {
68+
let querystring = "";
69+
if(!!per_page || !!page) {
70+
querystring = "?";
71+
let perPageClause = !per_page ? "" : `per_page=${per_page}`
72+
let pageClause = !page ? "" : `page=${page}`;
73+
querystring = `?${perPageClause}&${pageClause}`;
74+
}
75+
return callManagementApi<Auth0UserInfo[]>(`/users${querystring}`);
76+
}
77+
78+
export async function updateUser(userId: string, userInfoPatch: UserInfoPatch) {
79+
userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`;
80+
console.log("Patching from API");
81+
return callManagementApi<Auth0UserInfo>(`/users/${userId}`, "PATCH", JSON.stringify(userInfoPatch));
82+
}
83+
84+
async function callManagementApi<T>(path: string, method = "GET", body = "", refresh = false) : Promise<T> {
85+
let mgmtDomain = process.env.AUTH0_DOMAIN;
86+
let adminToken = await getAdminToken(refresh);
87+
let url = encodeURI(`https://${mgmtDomain}/api/v2${path}`);
88+
let options : any = {
89+
method,
90+
"headers": {
91+
"Authorization": `Bearer ${adminToken}`
92+
}
93+
};
94+
if(!!body) {
95+
options.headers["Content-Type"] = "application/json";
96+
options.body = body;
97+
}
98+
let metaResponse = await fetch(url, options);
99+
if(metaResponse.status === 200) {
100+
return await metaResponse.json();
101+
} else if (!refresh && metaResponse.status === 401) {
102+
return await callManagementApi<T>(path, method, body,true);
103+
} else {
104+
throw new Error(JSON.stringify(metaResponse));
105+
}
106+
}
107+
export async function getUserIdFromAuth0(authHeader: string) {
108+
try {
109+
let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, {
110+
"method": "GET",
111+
"headers": {"Authorization": authHeader}
112+
});
113+
let payload: any = await userResponse.json();
114+
let userId : string = !payload.sub ? "" : payload.sub;
115+
return userId;
116+
} catch (e) {
117+
throw e;
118+
}
119+
}
120+
121+
async function getAdminToken(refresh : boolean) {
122+
if(refresh || !moduleAdminToken) {
123+
let mgmtDomain = process.env.AUTH0_DOMAIN;
124+
125+
let refreshResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, {
126+
"method": "post",
127+
"headers": {
128+
"Content-Type": "application/json"
129+
},
130+
body: JSON.stringify({
131+
"grant_type": "client_credentials",
132+
"client_id": process.env.AUTH0_MGMT_CLIENT_ID,
133+
"client_secret": process.env.AUTH0_MGMT_CLIENT_SECRET,
134+
"audience": `https://${mgmtDomain}/api/v2/`,
135+
"scope": "read:users update:users read:users_app_metadata update:users_app_metadata"
136+
})
137+
});
138+
let refreshJson = await refreshResponse.json();
139+
moduleAdminToken = refreshJson.access_token
140+
}
141+
return moduleAdminToken;
142+
}

src/endpoints/businesses.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export interface BusinessUpdate {
5151
}
5252

5353
export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) {
54-
5554
app.get<AuthenticatedRequestByRegionId>(
5655
'/regions/:regionId/businesses',
5756
{schema: getBizSchema},

0 commit comments

Comments
 (0)