diff --git a/packages/lib-panoptes-js/src/experimental-auth.js b/packages/lib-panoptes-js/src/experimental-auth.js index afa3606fdd..4c9cc50ffa 100644 --- a/packages/lib-panoptes-js/src/experimental-auth.js +++ b/packages/lib-panoptes-js/src/experimental-auth.js @@ -145,6 +145,7 @@ async function signIn (login, password, _store) { // - This "authenticity token", as it will be later be called, prevents third // parties from simply replaying the HTTPs-encoded sign-in request. // - In our case, the CSRF token is provided Panoptes itself. + const request1 = new Request(`https://panoptes-staging.zooniverse.org/users/sign_in/?now=${Date.now()}`, { credentials: 'include', method: 'GET', @@ -160,7 +161,7 @@ async function signIn (login, password, _store) { // - These HTTP cookies identify requests as coming from us (or rather, from // our particular session. // - This is how request2 (submit username & password) and request3 (request - // bearer token for a logged in user) are magically linked and recognised + // bearer token for a logged-in user) are magically linked and recognised // as coming from the same person/session, even though request3 isn't // providing any login data explicitly via the JavaScript code. // - HTTP-only cookies can't be viewed or edited by JavaScript, as it happens. @@ -261,7 +262,7 @@ async function signIn (login, password, _store) { store.bearerToken = bearerToken, store.bearerTokenExpiry = bearerTokenExpiry store.refreshToken = refreshToken - _broadcastEvent('change', userData) + _broadcastEvent('change', userData, store) return userData @@ -275,7 +276,111 @@ async function signIn (login, password, _store) { Checks if there's a current, signed-in user. */ async function checkCurrentUser (_store) { + const store = _store || globalStore + + // Step 1: do we already have a user in the store? + if (store.userData) { + + // If yes, just return the user. + return store.userData + + } else { + // If no, let's ask Panoptes who's the current user. + console.log('Checking current user') + + let user = null + + try { + + // Step 2: get the bearer token. + // If user has previously signed in on this web browser, (e.g. they signed + // in on window 1, and then we attempt to get the bearer token on + // window 2,) then the Panoptes API will *automagically* know that the + // request is coming from the same signed-in user. + // This appears to be the result of certain HTTP-only cookies - + // _Panoptes_session and remember_user_token - being passed along with the + // request header. + + // TODO: figure out how the browser stores these http-only cookies. + // On Chrome 131, when opening a new window, the Applications > Cookies + // shows an empty list. But an initial request to /oauth/token will + // somehow magically contain the correct cookies in its header. 🤷‍♀️ + // (After the first response is received, the Cookies list in Chrome is + // updated to list the expected cookies.) + + const request1 = new Request(`https://panoptes-staging.zooniverse.org/oauth/token`, { + body: JSON.stringify({ + client_id: '535759b966935c297be11913acee7a9ca17c025f9f15520e7504728e71110a27', + grant_type: 'password', + }), + credentials: 'include', + method: 'POST', + headers: PANOPTES_HEADERS, + }) + const response1 = await fetch(request1) + + // if response is a 401, then there's no logged-in user. + if (response1.status === 401) { + return null + } + + // Extract data and check for errors. + if (!response1.ok) { + const jsonData1 = await response1.json() + const error = jsonData1?.error || 'No idea what went wrong; no specific error message detected.' + throw new Error(`Error from API. ${error}`) + } + const jsonData1 = await response1.json() + const bearerToken = jsonData1?.access_token // The bearer token is short-lived + const refreshToken = jsonData1?.refresh_token // The refresh token is used to get new bearer tokens. + const bearerTokenExpiry = Date.now() + (jsonData1?.expires_in * 1000) // Use Date.now() instead of response.created_at, because it keeps future "has expired?" comparisons consistent to the client's clock instead of the server's clock. + + if (!bearerToken || !refreshToken) { + // throw new Error('Impossible API response. access_token and/or refresh_token unavailable.') + } else if (jsonData1?.token_type !== 'Bearer') { + throw new Error('Impossible API response. Token wasn\'t of type "Bearer".') + } else if (isNaN(bearerTokenExpiry)) { + throw new Error('Impossible API response. Token expiry can\'t be calculated.') + } else if (bearerTokenExpiry <= Date.now()) { + throw new Error('Impossible API response. Token has already expired for some reason.') + } + + return + + const request2 = new Request(`https://panoptes-staging.zooniverse.org/api/me`, { + credentials: 'include', + method: 'GET', + headers: PANOPTES_HEADERS, + }) + + const response2 = await fetch(request2) + + // Extract data and check for errors. + console.log('+++ response2: ', response2) + console.log('+++ response2.json: ', await response2?.json()) + + /* + if (!response.ok) { + const jsonData2 = await response2.json() + const error = jsonData2?.error || 'No idea what went wrong; no specific error message detected.' + throw new Error(`Error from API. ${error}`) + } + const jsonData2 = await response2.json() + const userData = jsonData2?.users?.[0] + if (!userData) { + throw new Error('Impossible API response. No user returned.') + } else if (userData.login && userData.login !== login) { + throw new Error('Impossible API response. User returned is different from login attempt. Did you forget to sign out first?') + } + */ + + } catch (err) { + console.error('+++ checkCurrentUser error: ', err) + } + + return user + } } // Alias for checkCurrentUser