-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.js
More file actions
101 lines (90 loc) · 2.92 KB
/
auth.js
File metadata and controls
101 lines (90 loc) · 2.92 KB
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
/**
* @file Auth resolution for TPEN-Prompts.
*
* This tool never initiates login. A token is supplied externally — either via
* the parent transcription interface's `TPEN_ID_TOKEN` postMessage (when
* embedded as a splitscreen tool) or via `?idToken=` on the URL (when opened
* standalone from TPEN3 with a returnTo). Same localStorage key (`userToken`)
* and URL-param name (`idToken`) as tpen3-interfaces, so stored sessions are
* interchangeable.
*
* @author thehabes
*/
const TOKEN_KEY = 'userToken'
/**
* Restore `=` padding on a base64url string and swap URL-safe chars back to
* standard base64 so `atob` can decode it.
* @param {string} s
* @returns {string}
*/
function restorePadding(s) {
const pad = s.length % 4
if (pad) {
if (pad === 1) throw new Error('Invalid base64url length')
s += '===='.slice(0, 4 - pad)
}
return s.replace(/-/g, '+').replace(/_/g, '/')
}
/**
* Decode a JWT's payload segment. Does not verify the signature.
* @param {string} token
* @returns {Record<string, unknown>}
*/
function decodeJwt(token) {
return token ? JSON.parse(atob(restorePadding(token.split('.')[1]))) : {}
}
/**
* True if the token's `exp` claim is missing, non-numeric, or in the past.
* Any decode error is treated as expired.
* @param {string} token
* @returns {boolean}
*/
function isExpired(token) {
try {
const { exp } = decodeJwt(token)
if (typeof exp !== 'number' || !Number.isFinite(exp)) return true
return Date.now() >= exp * 1000
} catch {
return true
}
}
/** Remove `idToken` from the current URL without a navigation. */
function stripAuthParamsFromUrl() {
const url = new URL(location.href)
url.searchParams.delete('idToken')
history.replaceState(null, '', url.pathname + url.search + url.hash)
}
/**
* Return a valid token from the URL or localStorage, or null. Never redirects.
* When a URL token is present it's persisted and the URL is scrubbed.
* @returns {string|null}
*/
export function resolveToken() {
const urlToken = new URLSearchParams(location.search).get('idToken')
const stored = localStorage.getItem(TOKEN_KEY)
const candidate = urlToken ?? stored
if (urlToken) stripAuthParamsFromUrl()
if (!candidate || isExpired(candidate)) {
localStorage.removeItem(TOKEN_KEY)
return null
}
localStorage.setItem(TOKEN_KEY, candidate)
return candidate
}
/** Remove any stored token from `localStorage`. */
export function clearStoredToken() {
localStorage.removeItem(TOKEN_KEY)
}
/**
* Persist a token if it's present and unexpired. Clears storage otherwise.
* @param {string|null|undefined} token
* @returns {string|null} the stored token, or null when rejected.
*/
export function persistToken(token) {
if (!token || isExpired(token)) {
localStorage.removeItem(TOKEN_KEY)
return null
}
localStorage.setItem(TOKEN_KEY, token)
return token
}