Skip to content

Commit

Permalink
checkCurrentUser(): add 'fetch bearer token' action
Browse files Browse the repository at this point in the history
  • Loading branch information
shaunanoordin committed Dec 11, 2024
1 parent bf156fd commit bc83ea8
Showing 1 changed file with 107 additions and 2 deletions.
109 changes: 107 additions & 2 deletions packages/lib-panoptes-js/src/experimental-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down

0 comments on commit bc83ea8

Please sign in to comment.