Update /status endpoint to return available auth methods, fix socket auth, update openid to use username instead of email

This commit is contained in:
advplyr 2023-09-24 12:36:36 -05:00
parent 9922294507
commit f6de373388
4 changed files with 76 additions and 70 deletions

View File

@ -25,8 +25,11 @@
</div> </div>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40"> <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40">
<p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p> <p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
<p v-if="error" class="text-error text-center py-2">{{ error }}</p> <p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<form v-show="login_local" @submit.prevent="submitForm"> <form v-show="login_local" @submit.prevent="submitForm">
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
<ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" /> <ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" />
@ -37,7 +40,9 @@
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn> <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</form> </form>
<hr />
<div v-if="login_local && (login_google_oauth20 || login_openid)" class="w-full h-px bg-white bg-opacity-10 my-4" />
<div class="w-full flex py-3"> <div class="w-full flex py-3">
<a v-show="login_google_oauth20" :href="`http://localhost:3333/auth/google?callback=${currentUrl}`"> <a v-show="login_google_oauth20" :href="`http://localhost:3333/auth/google?callback=${currentUrl}`">
<ui-btn color="primary" class="leading-none">Login with Google</ui-btn> <ui-btn color="primary" class="leading-none">Login with Google</ui-btn>
@ -106,6 +111,9 @@ export default {
computed: { computed: {
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
},
googleAuthUri() {
return `${process.env.serverUrl}/auth/openid?callback=${currentUrl}`
} }
}, },
methods: { methods: {
@ -210,14 +218,16 @@ export default {
this.processing = true this.processing = true
this.$axios this.$axios
.$get('/status') .$get('/status')
.then((res) => { .then((data) => {
this.processing = false this.processing = false
this.isInit = res.isInit this.isInit = data.isInit
this.showInitScreen = !res.isInit this.showInitScreen = !data.isInit
this.$setServerLanguageCode(res.language) this.$setServerLanguageCode(data.language)
if (this.showInitScreen) { if (this.showInitScreen) {
this.ConfigPath = res.ConfigPath || '' this.ConfigPath = data.ConfigPath || ''
this.MetadataPath = res.MetadataPath || '' this.MetadataPath = data.MetadataPath || ''
} else {
this.updateLoginVisibility(data.authMethods || [])
} }
}) })
.catch((error) => { .catch((error) => {
@ -226,43 +236,34 @@ export default {
this.criticalError = 'Status check failed' this.criticalError = 'Status check failed'
}) })
}, },
async updateLoginVisibility() { updateLoginVisibility(authMethods) {
await this.$axios if (authMethods.includes('local') || !authMethods.length) {
.$get('/auth_methods')
.then((response) => {
if (response.includes('local')) {
this.login_local = true this.login_local = true
} else { } else {
this.login_local = false this.login_local = false
} }
if (response.includes('google-oauth20')) { if (authMethods.includes('google-oauth20')) {
this.login_google_oauth20 = true this.login_google_oauth20 = true
} else { } else {
this.login_google_oauth20 = false this.login_google_oauth20 = false
} }
if (response.includes('openid')) { if (authMethods.includes('openid')) {
this.login_openid = true this.login_openid = true
} else { } else {
this.login_openid = false this.login_openid = false
} }
})
.catch((error) => {
console.error('Failed', error.response)
return false
})
} }
}, },
async mounted() { async mounted() {
this.$nextTick(async () => await this.updateLoginVisibility())
if (new URLSearchParams(window.location.search).get('setToken')) { if (new URLSearchParams(window.location.search).get('setToken')) {
localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken'))
} }
if (localStorage.getItem('token')) { if (localStorage.getItem('token')) {
var userfound = await this.checkAuth() if (await this.checkAuth()) return // if valid user no need to check status
if (userfound) return // if valid user no need to check status
} }
this.checkStatus() this.checkStatus()
} }
} }

View File

@ -64,10 +64,9 @@ class Auth {
(async function (issuer, profile, done) { (async function (issuer, profile, done) {
// TODO: do we want to create the users which does not exist? // TODO: do we want to create the users which does not exist?
// get user by email const user = await Database.userModel.getUserByUsername(profile.username)
var user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase())
if (!user || !user.isActive) { if (!user?.isActive) {
// deny login // deny login
done(null, null) done(null, null)
return return
@ -106,9 +105,10 @@ class Auth {
} }
/** /**
* Stores the client's choise how the login callback should happen in temp cookies. * Stores the client's choice how the login callback should happen in temp cookies
* @param {*} req Request object. *
* @param {*} res Response object. * @param {import('express').Request} req
* @param {import('express').Response} res
*/ */
paramsToCookies(req, res) { paramsToCookies(req, res) {
if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { if (req.query.isRest && req.query.isRest.toLowerCase() == "true") {
@ -140,12 +140,12 @@ class Auth {
} }
} }
/** /**
* Informs the client in the right mode about a successfull login and the token * Informs the client in the right mode about a successfull login and the token
* (clients choise is restored from cookies). * (clients choise is restored from cookies).
* @param {*} req Request object. *
* @param {*} res Response object. * @param {import('express').Request} req
* @param {import('express').Response} res
*/ */
async handleLoginSuccessBasedOnCookie(req, res) { async handleLoginSuccessBasedOnCookie(req, res) {
// get userLogin json (information about the user, server and the session) // get userLogin json (information about the user, server and the session)
@ -170,16 +170,15 @@ class Auth {
/** /**
* Creates all (express) routes required for authentication. * Creates all (express) routes required for authentication.
* @param {express.Router} router *
* @param {import('express').Router} router
*/ */
async initAuthRoutes(router) { async initAuthRoutes(router) {
// Local strategy login route (takes username and password) // Local strategy login route (takes username and password)
router.post('/login', passport.authenticate('local'), router.post('/login', passport.authenticate('local'), async (req, res) => {
(async function (req, res) {
// return the user login response json if the login was successfull // return the user login response json if the login was successfull
res.json(await this.getUserLoginResponsePayload(req.user)) res.json(await this.getUserLoginResponsePayload(req.user))
}).bind(this) })
)
// google-oauth20 strategy login route (this redirects to the google login) // google-oauth20 strategy login route (this redirects to the google login)
router.get('/auth/google', (req, res, next) => { router.get('/auth/google', (req, res, next) => {
@ -222,18 +221,13 @@ class Auth {
} }
}) })
}) })
// Get avilible auth methods
router.get('/auth_methods', (req, res) => {
res.json(global.ServerSettings.authActiveAuthMethods)
})
} }
/** /**
* middleware to use in express to only allow authenticated users. * middleware to use in express to only allow authenticated users.
* @param {express.Request} req * @param {import('express').Request} req
* @param {express.Response} res * @param {import('express').Response} res
* @param {express.NextFunction} next * @param {import('express').NextFunction} next
*/ */
isAuthenticated(req, res, next) { isAuthenticated(req, res, next) {
// check if session cookie says that we are authenticated // check if session cookie says that we are authenticated
@ -246,18 +240,20 @@ class Auth {
} }
/** /**
* Function to generate a jwt token for a given user. * Function to generate a jwt token for a given user
*
* @param {Object} user * @param {Object} user
* @returns the token. * @returns {string} token
*/ */
generateAccessToken(user) { generateAccessToken(user) {
return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret)
} }
/** /**
* Function to validate a jwt token for a given user. * Function to validate a jwt token for a given user
*
* @param {string} token * @param {string} token
* @returns the tokens data. * @returns {Object} tokens data
*/ */
static validateAccessToken(token) { static validateAccessToken(token) {
try { try {
@ -365,9 +361,10 @@ class Auth {
} }
/** /**
* Return the login info payload for a user. * Return the login info payload for a user
* @param {string} username *
* @returns {Promise<string>} jsonPayload * @param {Object} user
* @returns {Promise<Object>} jsonPayload
*/ */
async getUserLoginResponsePayload(user) { async getUserLoginResponsePayload(user) {
const libraryIds = await Database.libraryModel.getAllLibraryIds() const libraryIds = await Database.libraryModel.getAllLibraryIds()

View File

@ -238,7 +238,8 @@ class Server {
// server has been initialized if a root user exists // server has been initialized if a root user exists
const payload = { const payload = {
isInit: Database.hasRootUser, isInit: Database.hasRootUser,
language: Database.serverSettings.language language: Database.serverSettings.language,
authMethods: Database.serverSettings.authActiveAuthMethods
} }
if (!payload.isInit) { if (!payload.isInit) {
payload.ConfigPath = global.ConfigPath payload.ConfigPath = global.ConfigPath

View File

@ -146,24 +146,31 @@ class SocketAuthority {
}) })
} }
// When setting up a socket connection the user needs to be associated with a socket id /**
// for this the client will send a 'auth' event that includes the users API token * When setting up a socket connection the user needs to be associated with a socket id
* for this the client will send a 'auth' event that includes the users API token
*
* @param {SocketIO.Socket} socket
* @param {string} token JWT
*/
async authenticateSocket(socket, token) { async authenticateSocket(socket, token) {
// we don't use passport to authenticate the jwt we get over the socket connection. // we don't use passport to authenticate the jwt we get over the socket connection.
// it's easier to directly verify/decode it. // it's easier to directly verify/decode it.
const token_data = Auth.validateAccessToken(token) const token_data = Auth.validateAccessToken(token)
if (!token_data || !token_data.id) {
if (!token_data?.userId) {
// Token invalid // Token invalid
Logger.error('Cannot validate socket - invalid token') Logger.error('Cannot validate socket - invalid token')
return socket.emit('invalid_token') return socket.emit('invalid_token')
} }
// get the user via the id from the decoded jwt. // get the user via the id from the decoded jwt.
const user = await Database.userModel.getUserById(token_data.id) const user = await Database.userModel.getUserByIdOrOldId(token_data.userId)
if (!user) { if (!user) {
// user not found // user not found
Logger.error('Cannot validate socket - invalid token') Logger.error('Cannot validate socket - invalid token')
return socket.emit('invalid_token') return socket.emit('invalid_token')
} }
const client = this.clients[socket.id] const client = this.clients[socket.id]
if (!client) { if (!client) {
Logger.error(`[SocketAuthority] Socket for user ${user.username} has no client`) Logger.error(`[SocketAuthority] Socket for user ${user.username} has no client`)