PoC OIDC frontend

This commit is contained in:
Spitfireap
2024-06-22 19:44:51 +02:00
parent a9f59c98ee
commit c106ecb649
8 changed files with 88 additions and 147 deletions
-105
View File
@@ -1,105 +0,0 @@
<script setup lang="js">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { useAuthStore } from '@/stores'
import rules from '@/plugins/rules.js'
const loading = ref(false)
const rememberMe = ref(false)
const username = ref('')
const password = ref('')
const isPasswordvisible = ref(false)
const loginForm = ref()
const errors = ref([])
const authStore = useAuthStore()
const router = useRouter()
const { $gettext } = useGettext()
async function authenticate() {
const { valid } = await loginForm.value.validate()
if (!valid) {
return
}
loading.value = true
const payload = {
username: username.value,
password: password.value,
rememberMe: rememberMe.value,
}
authStore
.login(payload)
.then(() => {
if (authStore.authUser.role === 'SimpleUsers') {
router.push({ name: 'AccountSettings' })
} else {
router.push({ name: 'Dashboard' })
}
})
.catch((err) => {
loading.value = false
if (err.response.status === 401) {
errors.value = [$gettext('Invalid username and/or password')]
} else if (err.response.status === 429) {
errors.value = [
$gettext('Too many unsuccessful attempts, please try later.'),
]
}
})
}
</script>
<template>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col cols="12" sm="10" md="8" lg="8" class="text-center">
<h4 class="text-primary mb-10 text-h4">
{{ $gettext('Identification') }}
</h4>
<v-form ref="loginForm" @submit.prevent="authenticate">
<v-text-field
v-model="username"
variant="outlined"
:label="$gettext('Username')"
prepend-inner-icon="mdi-account-outline"
:rules="[rules.required]"
/>
<v-text-field
v-model="password"
:type="isPasswordvisible ? 'text' : 'password'"
prepend-inner-icon="mdi-lock-outline"
:label="$gettext('Password')"
variant="outlined"
:append-inner-icon="isPasswordvisible ? 'mdi-eye-off' : 'mdi-eye'"
:rules="[rules.required, rules.minLength(6)]"
:error-messages="errors"
@click:append-inner="isPasswordvisible = !isPasswordvisible"
/>
<v-checkbox v-model="rememberMe" :label="$gettext('Remember me')" />
<v-btn
block
color="primary"
size="large"
type="submit"
:loading="loading"
class="mb-5"
>
{{ $gettext('Connect') }}
</v-btn>
</v-form>
<v-btn
block
variant="outlined"
size="large"
:to="{ name: 'PasswordRecovery' }"
>
{{ $gettext('Forgot password') }}
</v-btn>
</v-col>
</v-row>
</v-container>
</template>
+12 -1
View File
@@ -17,6 +17,11 @@ const routes = [
name: 'TwoFA',
component: () => import('@/views/login/TwoFA.vue'),
},
{
path: 'logged',
name: 'LoginCallback',
component: () => import('@/views/login/LoginCallbackView.vue'),
},
{
path: 'password_recovery',
name: 'PasswordRecovery',
@@ -291,8 +296,14 @@ const router = createRouter({
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth !== undefined) {
const previousPage = window.location.href
sessionStorage.setItem('previousPage', previousPage)
const authStore = useAuthStore()
authStore.initialize().then(() => {
authStore.initialize().then((res) => {
if (res === null) {
next({name: 'Login'})
return
}
authStore.validateAccess()
if (to.meta.allowedRoles !== undefined) {
if (to.meta.allowedRoles.indexOf(authStore.authUser.role) === -1) {
+40 -18
View File
@@ -3,6 +3,7 @@ import Cookies from 'js-cookie'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import gettext from '@/plugins/gettext'
import router from '@/router/index.js'
import { UserManager } from 'oidc-client-ts'
import repository from '@/api/repository'
@@ -14,16 +15,16 @@ export const useAuthStore = defineStore('auth', () => {
const authUser = ref({})
const isAuthenticated = ref(false)
const manager = new UserManager({
authority: '/api/o',
authority: 'http://localhost:8000/api/o',
client_id: 'LVQbfIIX3khWR3nDvix1u9yEGHZUxcx53bhJ7FlD',
// Is client_secret necessary?
client_secret:
'fXzG7tq23eXrM7RK41pXHRseuRWnxe4gV0p4A4YXJlF9jva7QfsDft1g7ESYU9AXR35QE0lo2Ti696OXsqb9JHLjtrPxaYmgMSRmRwLCce5hVr3FB2QhfSOThFYhcuuR',
metadata: {
authorization_endpoint: '/api/o/authorize/',
token_endpoint: '/api/o/token/',
},
scope: 'read write',
redirect_uri: 'http://localhost:3000/login/logged',
response_type: 'code',
scope: 'openid read write',
automaticSilentRenew: true,
accessTokenExpiringNotificationTime: 60,
monitorSession: true,
filterProtocolClaims: true,
loadUserInfo: true,
})
const userHasMailbox = computed(() => {
@@ -40,11 +41,11 @@ export const useAuthStore = defineStore('auth', () => {
async function initialize() {
if (isAuthenticated.value) {
return
return null
}
const user = await manager.getUser()
if (!user) {
return
return null
}
repository.defaults.headers.common.Authorization = `Bearer ${user.access_token}`
repository.defaults.headers.post['Content-Type'] = 'application/json'
@@ -58,18 +59,38 @@ export const useAuthStore = defineStore('auth', () => {
}
}
async function login(payload) {
await manager.signinResourceOwnerCredentials(payload)
return initialize()
// const cookiesAttributes = { sameSite: 'strict' }
// if (payload.rememberMe) {
// cookiesAttributes.expires = 90
// }
async function login() {
try {
await manager.signinRedirect();
} catch (error) {
console.error('Error logging in:', error)
}
}
async function completeLogin() {
try {
const user = await manager.signinRedirectCallback()
isAuthenticated.value = true
const previousPage = sessionStorage.getItem('previousPage');
// Redirect the user to the previous page if available
if (previousPage) {
window.location.href = previousPage;
} else {
// Redirect to a default page if the previous page is not available
router.push({name : 'Dashboard'});
}
return user
} catch (error) {
console.error('Error completing login:', error)
return null
}
}
async function $reset() {
delete repository.defaults.headers.common.Authorization
authUser.value = {}
isAuthenticated.value = false
//TODO: Call the logout callback of OIDC and log out from the IdP
}
async function updateAccount(data) {
@@ -101,6 +122,7 @@ export const useAuthStore = defineStore('auth', () => {
return {
authUser,
completeLogin,
isAuthenticated,
userHasMailbox,
validateAccess,
@@ -0,0 +1,10 @@
<script setup lang="js">
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores'
const authStore = useAuthStore()
onMounted(() => authStore.completeLogin())
</script>
<template>
</template>
+17 -22
View File
@@ -1,26 +1,21 @@
<script setup lang="js">
import { onMounted } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useAuthStore } from '@/stores'
const authStore = useAuthStore()
const { $gettext } = useGettext()
onMounted(() => authStore.login())
</script>
<template>
<template v-if="smAndDown">
<LoginForm />
</template>
<template v-else>
<v-row class="fill-height ma-0">
<v-col class="bg-primary">
<v-img
src="@/assets/Modoboa_RVB-BLANC-SANS.png"
max-width="300"
class="mt-10 ml-8"
/>
</v-col>
<v-col class="text-center">
<LoginForm />
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col>
<h1>{{ $gettext('Attempting to log you in.') }}</h1>
</v-col>
</v-row>
</template>
</v-container>
</template>
<script setup>
import LoginForm from '@/components/login/LoginForm.vue'
import { useDisplay } from 'vuetify'
const { smAndDown } = useDisplay()
</script>