-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🛂 Port authentication related code from v3 API
The code is ported from hpc_service repo
- Loading branch information
Showing
12 changed files
with
1,683 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* Lightweight in-memory cache to speed-up authorization processes. | ||
* | ||
* TODO: extend this with some cross-container cache such as redis or memcached | ||
*/ | ||
|
||
import { createHash } from 'crypto'; | ||
|
||
const sha256 = (str: string) => createHash('sha256').update(str).digest('hex'); | ||
|
||
export type HIDInfo = { | ||
userId: string; | ||
given_name: string; | ||
family_name: string; | ||
email: string; | ||
}; | ||
|
||
export type HIDResponse = | ||
| { | ||
type: 'success'; | ||
info: HIDInfo; | ||
} | ||
| { | ||
type: 'forbidden'; | ||
message: string; | ||
}; | ||
|
||
type CachedValue<T> = { | ||
value: T; | ||
time: Date; | ||
}; | ||
|
||
export class HashTableCache<V> { | ||
/** | ||
* The number of milliseconds a value should remain valid for | ||
*/ | ||
private readonly cacheItemLifetimeMs: number; | ||
|
||
private map = new Map<string, CachedValue<V>>(); | ||
|
||
constructor(opts: { cacheItemLifetimeMs: number }) { | ||
this.cacheItemLifetimeMs = opts.cacheItemLifetimeMs; | ||
} | ||
|
||
public store = (key: string, value: V, cacheTime?: Date): void => { | ||
this.map.set(sha256(key), { | ||
value, | ||
time: cacheTime || new Date(), | ||
}); | ||
this.clearExpiredValues(); | ||
}; | ||
|
||
public get = (key: string): V | null => { | ||
const item = this.map.get(sha256(key)); | ||
|
||
if (item && item.time.getTime() + this.cacheItemLifetimeMs > Date.now()) { | ||
return item.value; | ||
} | ||
|
||
return null; | ||
}; | ||
|
||
public clearExpiredValues = (): void => { | ||
const now = Date.now(); | ||
for (const [key, item] of this.map.entries()) { | ||
if (item.time.getTime() + this.cacheItemLifetimeMs < now) { | ||
this.map.delete(key); | ||
} | ||
} | ||
}; | ||
|
||
public clear = (): void => this.map.clear(); | ||
|
||
public size = (): number => this.map.size; | ||
} | ||
|
||
export const HID_CACHE = new HashTableCache<HIDResponse>({ | ||
cacheItemLifetimeMs: 5 * 60 * 1000, // 5 minutes | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import * as t from 'io-ts'; | ||
import * as fetch from 'node-fetch'; | ||
|
||
import { URL } from 'url'; | ||
import Context from '../Context'; | ||
import { Request } from '../Request'; | ||
import { ForbiddenError } from '../util/error'; | ||
import { HIDInfo, HID_CACHE } from './cache'; | ||
|
||
// Export for mocking purposes inside test cases | ||
export { fetch }; | ||
|
||
const HID_ACCOUNT_INFO = t.type({ | ||
given_name: t.string, | ||
family_name: t.string, | ||
user_id: t.string, | ||
email: t.string, | ||
}); | ||
|
||
/** | ||
* Use the Authentication headers to get the HID token for the request, | ||
* and use that to determine what HID user the user is logged-in as by | ||
* requesting account information from HID using the token. | ||
* | ||
* If this has already been done for the current request, the previous result | ||
* will be returned and no requests will be made. Therefore it is safe and | ||
* efficient to use this function multiple times. | ||
*/ | ||
export const getHidInfo = async ( | ||
context: Pick<Context, 'token' | 'request' | 'config'> | ||
): Promise<(HIDInfo & Request['apiAuth']) | undefined> => { | ||
const { token, request, config } = context; | ||
if (!token || !request || !config) { | ||
return undefined; | ||
} | ||
const existing = HID_CACHE.get(token); | ||
|
||
if (existing) { | ||
if (existing.type === 'success') { | ||
request.apiAuth = { | ||
...(request.apiAuth || {}), | ||
userId: existing.info.userId, | ||
}; | ||
|
||
return existing.info; | ||
} | ||
|
||
throw new ForbiddenError(existing.message); | ||
} else { | ||
const accountUrl = new URL('/account.json', config.authBaseUrl); | ||
|
||
const res = await fetch.default(accountUrl, { | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}); | ||
|
||
if (!res.ok) { | ||
if (res.status === 401) { | ||
const r = await res.json(); | ||
const message = r.message || 'Invalid Token'; | ||
HID_CACHE.store(token, { type: 'forbidden', message }); | ||
throw new ForbiddenError(message); | ||
} else { | ||
throw new ForbiddenError( | ||
`Unexpected error from HID: ${res.statusText}` | ||
); | ||
} | ||
} | ||
|
||
const data = await res.json(); | ||
|
||
if (!HID_ACCOUNT_INFO.is(data)) { | ||
throw new ForbiddenError('Got invalid data from HID'); | ||
} | ||
|
||
const info: HIDInfo = { | ||
userId: data.user_id, | ||
family_name: data.family_name, | ||
given_name: data.given_name, | ||
email: data.email, | ||
}; | ||
HID_CACHE.store(token, { type: 'success', info }); | ||
|
||
request.apiAuth = { | ||
...(request.apiAuth || {}), | ||
userId: info.userId, | ||
}; | ||
|
||
return info; | ||
} | ||
}; |
Oops, something went wrong.