mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
PoC OIDC frontend
This commit is contained in:
@@ -28,6 +28,8 @@ services:
|
||||
dockerfile: docker/Dockerfile.dev
|
||||
ports:
|
||||
- 8000:8000
|
||||
env_file:
|
||||
- test_project/oidc_key
|
||||
environment:
|
||||
PYTHONPATH: ".."
|
||||
DB: "SQLITE"
|
||||
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
OIDC_RSA_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n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\nBdzfBSJDSvm22iXUM6TnKy5TksSayQ==\n-----END PRIVATE KEY-----\n"
|
||||
@@ -194,12 +194,17 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
# oAuth2 settings
|
||||
|
||||
OAUTH2_PROVIDER = {
|
||||
"OIDC_ENABLED": True,
|
||||
"OIDC_RP_INITIATED_LOGOUT_ENABLED": True,
|
||||
"OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT": True,
|
||||
"OIDC_RSA_PRIVATE_KEY": os.environ.get("OIDC_RSA_PRIVATE_KEY"),
|
||||
"SCOPES": {
|
||||
"openid": "OpenID Connect scope",
|
||||
"read": "Read scope",
|
||||
"write": "Write scope",
|
||||
"introspection": "Introspect token scope",
|
||||
},
|
||||
"DEFAULT_SCOPES": ["read", "write"],
|
||||
"DEFAULT_SCOPES": ["openid", "read", "write"],
|
||||
}
|
||||
|
||||
# Rest framework settings
|
||||
|
||||
Reference in New Issue
Block a user