mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-13 10:55:05 +02:00
Add support for returning refresh token for mobile clients
Some checks failed
Some checks failed
This commit is contained in:
parent
4d32a22de9
commit
8b995a179d
4 changed files with 35 additions and 14 deletions
|
@ -152,8 +152,11 @@ export const mutations = {
|
|||
setUser(state, user) {
|
||||
state.user = user
|
||||
if (user) {
|
||||
// Use accessToken from user if included in response (for login)
|
||||
if (user.accessToken) localStorage.setItem('token', user.accessToken)
|
||||
else {
|
||||
else if (localStorage.getItem('token')) {
|
||||
user.accessToken = localStorage.getItem('token')
|
||||
} else {
|
||||
console.error('No access token found for user', user)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -466,14 +466,29 @@ class Auth {
|
|||
// return the user login response json if the login was successfull
|
||||
const userResponse = await this.getUserLoginResponsePayload(req.user)
|
||||
|
||||
this.setRefreshTokenCookie(req, res, req.user.refreshToken)
|
||||
// Check if mobile app wants refresh token in response
|
||||
const returnTokens = req.query.return_tokens === 'true' || req.headers['x-return-tokens'] === 'true'
|
||||
|
||||
userResponse.user.refreshToken = returnTokens ? req.user.refreshToken : null
|
||||
userResponse.user.accessToken = req.user.accessToken
|
||||
|
||||
if (!returnTokens) {
|
||||
this.setRefreshTokenCookie(req, res, req.user.refreshToken)
|
||||
}
|
||||
|
||||
res.json(userResponse)
|
||||
})
|
||||
|
||||
// Refresh token route
|
||||
router.post('/auth/refresh', async (req, res) => {
|
||||
const refreshToken = req.cookies.refresh_token
|
||||
let refreshToken = req.cookies.refresh_token
|
||||
|
||||
// For mobile clients, the refresh token is sent in the authorization header
|
||||
let shouldReturnRefreshToken = false
|
||||
if (!refreshToken && req.headers.authorization?.startsWith('Bearer ')) {
|
||||
refreshToken = req.headers.authorization.split(' ')[1]
|
||||
shouldReturnRefreshToken = true
|
||||
}
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(401).json({ error: 'No refresh token provided' })
|
||||
|
@ -507,10 +522,12 @@ class Auth {
|
|||
return res.status(401).json({ error: 'User not found or inactive' })
|
||||
}
|
||||
|
||||
const newAccessToken = await this.rotateTokensForSession(session, user, req, res)
|
||||
const newTokens = await this.rotateTokensForSession(session, user, req, res)
|
||||
|
||||
user.accessToken = newAccessToken
|
||||
const userResponse = await this.getUserLoginResponsePayload(user)
|
||||
|
||||
userResponse.user.accessToken = newTokens.accessToken
|
||||
userResponse.user.refreshToken = shouldReturnRefreshToken ? newTokens.refreshToken : null
|
||||
res.json(userResponse)
|
||||
} catch (error) {
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
|
@ -961,7 +978,7 @@ class Auth {
|
|||
* @param {import('./models/User')} user
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @returns {Promise<string>} newAccessToken
|
||||
* @returns {Promise<{ accessToken:string, refreshToken:string }>}
|
||||
*/
|
||||
async rotateTokensForSession(session, user, req, res) {
|
||||
// Generate new tokens
|
||||
|
@ -978,7 +995,10 @@ class Auth {
|
|||
// Set new refresh token cookie
|
||||
this.setRefreshTokenCookie(req, res, newRefreshToken)
|
||||
|
||||
return newAccessToken
|
||||
return {
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: newRefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -996,7 +1016,7 @@ class Auth {
|
|||
// So rotate token for current session
|
||||
const currentSession = await Database.sessionModel.findOne({ where: { refreshToken: currentRefreshToken } })
|
||||
if (currentSession) {
|
||||
const newAccessToken = await this.rotateTokensForSession(currentSession, user, req, res)
|
||||
const newTokens = await this.rotateTokensForSession(currentSession, user, req, res)
|
||||
|
||||
// Invalidate all sessions for the user except the current one
|
||||
await Database.sessionModel.destroy({
|
||||
|
@ -1008,7 +1028,7 @@ class Auth {
|
|||
}
|
||||
})
|
||||
|
||||
return newAccessToken
|
||||
return newTokens.accessToken
|
||||
} else {
|
||||
Logger.error(`[Auth] No session found to rotate tokens for refresh token ${currentRefreshToken}`)
|
||||
}
|
||||
|
|
|
@ -336,6 +336,9 @@ class UserController {
|
|||
const newAccessToken = await this.auth.invalidateJwtSessionsForUser(user, req, res)
|
||||
if (newAccessToken) {
|
||||
user.accessToken = newAccessToken
|
||||
// Refresh tokens are only returned for mobile clients
|
||||
// Mobile apps currently do not use this API endpoint so always set to null
|
||||
user.refreshToken = null
|
||||
Logger.info(`[UserController] Invalidated JWT sessions for user ${user.username} and rotated tokens for current session`)
|
||||
} else {
|
||||
Logger.info(`[UserController] Invalidated JWT sessions for user ${user.username}`)
|
||||
|
|
|
@ -112,10 +112,6 @@ class User extends Model {
|
|||
this.updatedAt
|
||||
/** @type {import('./MediaProgress')[]?} - Only included when extended */
|
||||
this.mediaProgresses
|
||||
|
||||
// Temporary accessToken, not stored in database
|
||||
/** @type {string} */
|
||||
this.accessToken
|
||||
}
|
||||
|
||||
// Excludes "root" since their can only be 1 root user
|
||||
|
@ -526,7 +522,6 @@ class User extends Model {
|
|||
type: this.type,
|
||||
// TODO: Old non-expiring token
|
||||
token: this.type === 'root' && hideRootToken ? '' : this.token,
|
||||
accessToken: this.accessToken || null,
|
||||
mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [],
|
||||
seriesHideFromContinueListening: [...seriesHideFromContinueListening],
|
||||
bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue