Auth Handlers
Three handlers that complete the auth lifecycle after login: checkToken, refreshToken, and logout.
checkToken
Called before every session resume — CI HUB verifies the stored access token is still valid.
Make an API call to the platform's "current user" endpoint. On success, return user info via send(). On failure, return sendError() to force re-login.
Inputs
| Field | Description |
|---|---|
locals.authPayload | Stored access token (string or object) |
locals.adapterData | Adapter-specific data persisted from login |
Response
// Success — session resumes, plugin UI shows user display name
return send({ adapter: 'AdapterName', source: 'hostname-or-label', user: 'displayName' })
// Failure — forces re-login
return sendError(err, 400)The source field appears in the plugin UI connection list. Typically derived from the server URL hostname.
Patterns
Example - OAuth adapter, validates token via REST call:
checkToken: async (locals) => {
try {
const data = await callApi(locals, 'GET', '/api/v4/currentUser/', {})
return send({ adapter: 'ExampleAdapter', source: remoteSystemPrefix(locals), user: data.username })
} catch (err) {
return sendError(err, 400)
}
},Example - OAuth adapter, validates via SDK client:
checkToken: async (locals) => {
const accessToken = locals.authPayload
sstk.setAccessToken(accessToken)
try {
const usersApi = new sstk.UsersApi()
const user = await usersApi.getUser()
return send({ adapter: 'ExampleAdapter', source: remoteSystemPrefix, user: user.username })
} catch (err) {
return sendError(err, 400)
}
},Example - Credential-based adapter (API key/secret), no API call needed:
checkToken: async (locals) => {
try {
const cloudName = locals.authPayload.cloudName
return send({ adapter: 'ExampleAdapter', source: remoteSystemPrefix, user: cloudName })
} catch (err) {
return sendError(err, 400)
}
},For credential-based adapters where the token is an object (not an OAuth access token), extract the display name directly — no API validation needed.
UI Behavior
On success, the plugin UI restores the previous session and shows the user value in the connection header. On failure, CI HUB drops the session and shows the login screen.
refreshToken
Called when checkToken fails and a stored refresh token exists. Exchange the refresh token for a new access token.
Return the new token via sendAccessToken(). Return sendError() to abandon refresh and force re-login.
Inputs
| Field | Description |
|---|---|
locals.authPayload | Stored refresh token |
locals.adapterData | Adapter-specific data persisted from login |
Response
// Success — new access token stored, session resumes
return sendAccessToken(null, accessToken, expiresIn, refreshToken, refreshExpiresIn, adapterData)
// Failure — forces re-login
return sendError(err, 400)Parameter order for sendAccessToken: error, token, expiresIn, refreshToken, refreshExpiresIn, adapterData.
Patterns
Example - Standard OAuth refresh with adapterData preservation:
refreshToken: async (locals) => {
const adapterData = locals.adapterData
if (!adapterData.resourceEndpoint) {
return sendError('Could not refresh token, please re-add the connection.', 400)
}
try {
const oauth = getAuthClient(adapterData.resourceEndpoint)
const accessToken = oauth.createToken({
refresh_token: locals.authPayload
})
const { token } = await accessToken.refresh()
if (!token || !token.access_token) {
throw new Error('No access_token found')
}
return sendAccessToken(null, token.access_token, token.expires_in, undefined, undefined, adapterData)
} catch (err) {
return sendError(err, 400)
}
},Validate adapterData before attempting refresh — stale sessions missing required fields should error immediately with a clear message.
Example - OAuth refresh with client credentials:
refreshToken: async (locals) => {
const clientKey = config.get('exampleAdapter2.clientKey')
const clientSecret = config.get('exampleAdapter2.clientSecret')
const refreshToken = locals.authPayload
const oauth = getAuthClient()
const token = oauth.createToken({
refresh_token: refreshToken
})
try {
const newToken = (await token.refresh({
client_id: clientKey,
client_secret: clientSecret
})).token
const accessToken = newToken.access_token
const tokenExpiresIn = newToken.expires_in
return sendAccessToken(null, accessToken, tokenExpiresIn)
} catch (err) {
return sendAccessToken(err)
}
},Example - Credential-based adapter, no actual refresh:
refreshToken: async (locals) => {
try {
const token = locals.authPayload
const adapterData = locals.adapterData
return sendAccessToken(null, token, null, null, null, adapterData)
} catch (err) {
return sendError(err, 400)
}
},Credential-based adapters (API key + secret) have no expiring OAuth token. Re-send the same token and adapterData to keep the session alive.
UI Behavior
On success, the session resumes transparently — the user never sees a login screen. On failure, CI HUB falls back to the login flow.
logout
Called when the user disconnects from the adapter in the plugin UI. Revoke tokens if the platform supports it, then return sendStatus(200).
CI HUB deletes all stored tokens regardless of the handler's response.
Inputs
| Field | Description |
|---|---|
locals.authPayload | Stored access token |
requestData.body.refresh_token | Stored refresh token (if available) |
Response
// Always return 200
return sendStatus(200)Patterns
Example - Revokes refresh token via OAuth client:
logout: async (locals, requestData) => {
const access_token = locals.authPayload
const { refresh_token } = requestData.body
try {
const oauth = getAuthClient()
const token = oauth.createToken({ access_token, refresh_token })
await token.revoke('refresh_token')
return sendStatus(200)
} catch (err) {
return sendError(err, 400)
}
},Example - No revocation endpoint, return immediately:
logout: async (locals, requestData) => {
return sendStatus(200)
},Example - No revocation, wrapped in try/catch:
logout: async (locals, requestData) => {
try {
return sendStatus(200)
} catch (err) {
return sendError(err, 400)
}
},If the platform has no token revocation endpoint, sendStatus(200) is sufficient. CI HUB cleans up stored tokens on its side.
UI Behavior
After logout completes, the plugin UI removes the connection and returns to the adapter selection or login screen.