|
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"; |
3 | 3 |
|
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}; |
59 | 8 |
|
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); |
79 | 11 | } |
80 | 12 |
|
81 | | -type UserRole = {role: string}; |
82 | | -async function getUserRole(userId: string) : Promise<UserRole> { |
| 13 | +async function getUserRoleFromAuth0(userId: string) : Promise<string> { |
83 | 14 | 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; |
107 | 16 | } |
108 | 17 |
|
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) { |
163 | 19 | if(!authHeader) { |
164 | | - return {userAppId: "", admin: false, role: ""}; |
| 20 | + return null; |
165 | 21 | } 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; |
174 | 32 | } |
175 | | - return cachedAuth.data; |
176 | 33 | } |
177 | 34 | } |
178 | 35 |
|
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}; |
182 | 42 | } |
183 | 43 |
|
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; |
190 | 52 | } |
0 commit comments