|
9 | 9 | * OF ANY KIND, either express or implied. See the License for the specific language |
10 | 10 | * governing permissions and limitations under the License. |
11 | 11 | */ |
12 | | -import { decodeJwt } from 'jose'; |
| 12 | +import { createRemoteJWKSet, jwtVerify, jwksCache } from 'jose'; |
13 | 13 |
|
14 | 14 | export async function logout({ daCtx, env }) { |
15 | 15 | await Promise.all(daCtx.users.map((u) => env.DA_AUTH.delete(u.ident))); |
@@ -53,14 +53,77 @@ export async function setUser(userId, expiration, headers, env) { |
53 | 53 | return value; |
54 | 54 | } |
55 | 55 |
|
| 56 | +/** |
| 57 | + * Retrieve cached IMS keys from KV Store |
| 58 | + * @param {*} env |
| 59 | + * @param {string} keysUrl |
| 60 | + * @returns {Promise<import('jose').ExportedJWKSCache>} |
| 61 | + */ |
| 62 | +async function getPreviouslyCachedJWKS(env, keysUrl) { |
| 63 | + const cachedJwks = await env.DA_AUTH.get(keysUrl); |
| 64 | + if (!cachedJwks) return {}; |
| 65 | + return JSON.parse(cachedJwks); |
| 66 | +} |
| 67 | + |
| 68 | +/** |
| 69 | + * Store new set of IMS keys in the KV Store |
| 70 | + * @param {*} env |
| 71 | + * @param {string} keysUrl |
| 72 | + * @param {import('jose').ExportedJWKSCache} keysCache |
| 73 | + * @returns {Promise<void>} |
| 74 | + */ |
| 75 | +async function storeJWSInCache(env, keysUrl, keysCache) { |
| 76 | + try { |
| 77 | + await env.DA_AUTH.put( |
| 78 | + keysUrl, |
| 79 | + JSON.stringify(keysCache), |
| 80 | + { |
| 81 | + expirationTtl: 24 * 60 * 60, // 24 hours in seconds |
| 82 | + }, |
| 83 | + ); |
| 84 | + } catch (err) { |
| 85 | + // An error may be thrown if a write to the same key is made within 1 second |
| 86 | + // eslint-disable-next-line no-console |
| 87 | + console.error('Failed to store keys in cache', err); |
| 88 | + } |
| 89 | +} |
| 90 | + |
56 | 91 | export async function getUsers(req, env) { |
57 | 92 | const authHeader = req.headers?.get('authorization'); |
58 | 93 | if (!authHeader) return [{ email: 'anonymous' }]; |
59 | 94 |
|
60 | 95 | async function parseUser(token) { |
61 | 96 | if (!token || token.trim().length === 0) return { email: 'anonymous' }; |
62 | 97 |
|
63 | | - const { user_id: userId, created_at: createdAt, expires_in: expiresIn } = decodeJwt(token); |
| 98 | + let payload; |
| 99 | + try { |
| 100 | + const keysURL = `${env.IMS_ORIGIN}/ims/keys`; |
| 101 | + |
| 102 | + const keysCache = await getPreviouslyCachedJWKS(env, keysURL); |
| 103 | + const { uat } = keysCache; |
| 104 | + |
| 105 | + const jwks = createRemoteJWKSet( |
| 106 | + new URL(keysURL), |
| 107 | + { |
| 108 | + [jwksCache]: keysCache, |
| 109 | + cacheMaxAge: 24 * 60 * 60 * 1000, // 24 hours in milliseconds |
| 110 | + }, |
| 111 | + ); |
| 112 | + |
| 113 | + ({ payload } = await jwtVerify(token, jwks)); |
| 114 | + |
| 115 | + if (uat !== keysCache.uat) { |
| 116 | + await storeJWSInCache(env, keysURL, keysCache); |
| 117 | + } |
| 118 | + } catch (e) { |
| 119 | + // eslint-disable-next-line no-console |
| 120 | + console.log('IMS token offline verification failed', e); |
| 121 | + return { email: 'anonymous' }; |
| 122 | + } |
| 123 | + |
| 124 | + if (!payload) return { email: 'anonymous' }; |
| 125 | + |
| 126 | + const { user_id: userId, created_at: createdAt, expires_in: expiresIn } = payload; |
64 | 127 | const expires = Number(createdAt) + Number(expiresIn); |
65 | 128 | const now = Math.floor(new Date().getTime() / 1000); |
66 | 129 |
|
|
0 commit comments