Skip to content

Commit

Permalink
feat: add OIDC auth - Ref gestion-de-projet#2091
Browse files Browse the repository at this point in the history
* feat: add OIDC auth starter

* fix: add missed imports

* fix: update oidc login call and removed oidc tokens constants

* feat: add authentication by oidc code

* refactor: rename auth functions

* fix: style oidc authenticating template

* fix: typo in token variable name

* fix: remove circular progress from OIDC login button

* style: connexion via OIDC

* style: styled keycloak login button - Ref gestion-de-projet#2091

---------

Co-authored-by: hicham <hicham.taroq-ext@aphp.fr>
Co-authored-by: manelleg <manelle.gueriouz@aphp.fr>
  • Loading branch information
3 people authored Jun 8, 2023
1 parent dbdd5a1 commit 0a11361
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/assets/icones/keycloak.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/components/Routes/AutoLogoutContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { close as closeAction, open as openAction } from 'state/autoLogout'
import { useAppDispatch, useAppSelector } from 'state'
import { logout as logoutAction } from 'state/me'

import { ACCES_TOKEN, BACK_API_URL, REFRESH_TOKEN, REFRESH_TOKEN_INTERVAL, SESSION_TIMEOUT } from '../../constants'
import { ACCESS_TOKEN, BACK_API_URL, REFRESH_TOKEN, REFRESH_TOKEN_INTERVAL, SESSION_TIMEOUT } from '../../constants'

import useStyles from './styles'

Expand Down Expand Up @@ -78,7 +78,7 @@ const AutoLogoutContainer = () => {
const res = await axios.post(`${BACK_API_URL}/accounts/refresh/`)

if (res.status === 200) {
localStorage.setItem(ACCES_TOKEN, res.data.access)
localStorage.setItem(ACCESS_TOKEN, res.data.access)
localStorage.setItem(REFRESH_TOKEN, res.data.refresh)
} else {
logout()
Expand Down
2 changes: 0 additions & 2 deletions src/components/Routes/LeftSideBar/LeftSideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ const LeftSideBar: React.FC<{ open?: boolean }> = (props) => {
>
<IconButton
onClick={() => {
localStorage.clear()
dispatch(logoutAction())
navigate('/')
}}
Expand All @@ -170,7 +169,6 @@ const LeftSideBar: React.FC<{ open?: boolean }> = (props) => {
<Tooltip title="Se déconnecter">
<IconButton
onClick={() => {
localStorage.clear()
dispatch(logoutAction())
navigate('/')
}}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Routes/PrivateRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { gql } from 'apollo-boost'

import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material'

import { ACCES_TOKEN } from '../../constants'
import { ACCESS_TOKEN } from '../../constants'

import { useAppSelector, useAppDispatch } from '../../state'
import { login } from '../../state/me'
Expand All @@ -24,7 +24,7 @@ const PrivateRoute: React.FC = () => {
const me = useAppSelector((state) => state.me)
const dispatch = useAppDispatch()
const location = useLocation()
const authToken = localStorage.getItem(ACCES_TOKEN)
const authToken = localStorage.getItem(ACCESS_TOKEN)

const [allowRedirect, setRedirection] = useState(false)

Expand Down
15 changes: 14 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
export let BOOLEANTRUE = 'true'

export const ACCES_TOKEN = 'access'
export const ACCESS_TOKEN = 'access'
export const REFRESH_TOKEN = 'refresh'

export const OIDC_PROVIDER_URL = import.meta.env.DEV
? import.meta.env.VITE_OIDC_PROVIDER_URL
: '{VITE_OIDC_PROVIDER_URL}'
export const OIDC_REDIRECT_URI = import.meta.env.DEV
? import.meta.env.VITE_OIDC_REDIRECT_URI
: '{VITE_OIDC_REDIRECT_URI}'
export const OIDC_RESPONSE_TYPE = import.meta.env.DEV
? import.meta.env.VITE_OIDC_RESPONSE_TYPE
: '{VITE_OIDC_RESPONSE_TYPE}'
export const OIDC_CLIENT_ID = import.meta.env.DEV ? import.meta.env.VITE_OIDC_CLIENT_ID : '{VITE_OIDC_CLIENT_ID}'
export const OIDC_SCOPE = import.meta.env.DEV ? import.meta.env.VITE_OIDC_SCOPE : '{VITE_OIDC_SCOPE}'
export const OIDC_STATE = import.meta.env.DEV ? import.meta.env.VITE_OIDC_STATE : '{VITE_OIDC_STATE}'

export const BACK_API_URL = import.meta.env.DEV ? import.meta.env.VITE_BACK_API_URL : '{VITE_BACK_API_URL}'
export const REQUEST_API_URL = import.meta.env.DEV ? import.meta.env.VITE_REQUEST_API_URL : '{VITE_REQUEST_API_URL}'
export const FHIR_API_URL = import.meta.env.DEV ? import.meta.env.VITE_FHIR_API_URL : '{VITE_FHIR_API_URL}'
Expand Down
23 changes: 20 additions & 3 deletions src/services/aphp/servicePractitioner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AxiosResponse } from 'axios'
import apiBackend from 'services/apiBackend'
import { ACCESS_TOKEN, REFRESH_TOKEN } from '../../constants'

export interface IServicePractitioner {
/**
Expand All @@ -11,7 +12,8 @@ export interface IServicePractitioner {
*
* Retourne la reponse de Axios
*/
authenticate: (username: string, password: string) => Promise<any>
authenticateWithCredentials: (username: string, password: string) => Promise<any>
authenticateWithCode: (code: string) => Promise<any>

/**
* Cette fonction permet d'appeler la route de logout
Expand Down Expand Up @@ -46,20 +48,35 @@ export interface IServicePractitioner {
}

const servicePractitioner: IServicePractitioner = {
authenticate: async (username, password) => {
authenticateWithCredentials: async (username, password) => {
try {
const formData = new FormData()
formData.append('username', username.toString())
formData.append('password', password)

return await apiBackend.post(`/accounts/login/`, formData)
} catch (error) {
console.error("erreur lors de l'exécution de la fonction authenticate", error)
console.error('Error authenticating with credentials', error)
return error
}
},

authenticateWithCode: async (authCode: string) => {
try {
const res = await apiBackend.post(`/auth/oidc/login`, { auth_code: authCode })
if (res.status === 200) {
localStorage.setItem(ACCESS_TOKEN, res.data.jwt.access)
localStorage.setItem(REFRESH_TOKEN, res.data.jwt.refresh)
}
return res
} catch (error) {
console.error('Error authenticating with an authorization code', error)
return error
}
},

logout: async () => {
localStorage.clear()
await apiBackend.post(`/accounts/logout/`)
},

Expand Down
4 changes: 2 additions & 2 deletions src/services/apiBackend.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios'
import { ACCES_TOKEN, BACK_API_URL } from '../constants'
import { ACCESS_TOKEN, BACK_API_URL } from '../constants'

const apiBackend = axios.create({
baseURL: BACK_API_URL,
Expand All @@ -9,7 +9,7 @@ const apiBackend = axios.create({
})

apiBackend.interceptors.request.use((config) => {
const token = localStorage.getItem(ACCES_TOKEN)
const token = localStorage.getItem(ACCESS_TOKEN)
config.headers.Authorization = `Bearer ${token}`
return config
})
Expand Down
4 changes: 2 additions & 2 deletions src/services/apiFhir.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios'
import { ACCES_TOKEN, FHIR_API_URL } from '../constants'
import { ACCESS_TOKEN, FHIR_API_URL } from '../constants'

const apiFhir = axios.create({
baseURL: FHIR_API_URL,
Expand All @@ -10,7 +10,7 @@ const apiFhir = axios.create({
})

apiFhir.interceptors.request.use((config) => {
const token = localStorage.getItem(ACCES_TOKEN)
const token = localStorage.getItem(ACCESS_TOKEN)
config.headers.Authorization = `Bearer ${token}`
return config
})
Expand Down
79 changes: 68 additions & 11 deletions src/views/Login/Login.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import localforage from 'localforage'
import {
Expand All @@ -21,15 +21,27 @@ import NoRights from 'components/ErrorView/NoRights'

import logo from 'assets/images/logo-login.png'
import logoAPHP from 'assets/images/logo-aphp.png'
import { ReactComponent as Keycloak } from 'assets/icones/keycloak.svg'

import { useAppDispatch } from 'state'
import { login as loginAction } from 'state/me'
import { ACCES_TOKEN, REFRESH_TOKEN } from '../../constants'
import {
ACCESS_TOKEN,
REFRESH_TOKEN,
OIDC_CLIENT_ID,
OIDC_PROVIDER_URL,
OIDC_REDIRECT_URI,
OIDC_RESPONSE_TYPE,
OIDC_SCOPE,
OIDC_STATE
} from '../../constants'

import services from 'services/aphp'

import useStyles from './styles'
import { getDaysLeft } from '../../utils/formatDate'
import Welcome from '../Welcome/Welcome'
import clsx from 'clsx'

const ErrorSnackBarAlert = ({ open, setError, errorMessage }) => {
const _setError = () => {
Expand Down Expand Up @@ -97,11 +109,19 @@ const Login = () => {
const [error, setError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [open, setOpen] = useState(false)
const [authCode, setAuthCode] = useState(undefined)
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')

React.useEffect(() => {
useEffect(() => {
localforage.setItem('persist:root', '')
if (code) setAuthCode(code)
}, [])

useEffect(() => {
if (authCode) login()
}, [authCode])

const getPractitionerData = async (practitioner, lastConnection, maintenance, accessExpirations = []) => {
if (practitioner) {
const practitionerPerimeters = await services.perimeters.getPerimeters()
Expand Down Expand Up @@ -169,12 +189,19 @@ const Login = () => {
if (loading) return
setLoading(true)

if (!username || !password) {
setLoading(false)
return setError(true), setErrorMessage("L'un des champs nom d'utilisateur ou mot de passe est vide.")
}
let response = null

const response = await services.practitioner.authenticate(username, password)
if (authCode) {
response = await services.practitioner.authenticateWithCode(authCode)
} else {
if (!username || !password) {
setLoading(false)
return setError(true), setErrorMessage("L'un des champs nom d'utilisateur ou mot de passe est vide.")
}
if (username && password) {
response = await services.practitioner.authenticateWithCredentials(username, password)
}
}

if (!response) {
setLoading(false)
Expand Down Expand Up @@ -214,10 +241,10 @@ const Login = () => {
const { status, data = {} } = response

if (status === 200) {
localStorage.setItem(ACCES_TOKEN, data.jwt.access)
localStorage.setItem(ACCESS_TOKEN, data.jwt.access)
localStorage.setItem(REFRESH_TOKEN, data.jwt.refresh)

const practitioner = await services.practitioner.fetchPractitioner(username)
const practitioner = await services.practitioner.fetchPractitioner(data.user.provider_username)

if (!practitioner || practitioner.error || !practitioner.response || practitioner.response.status !== 200) {
setLoading(false)
Expand Down Expand Up @@ -266,9 +293,28 @@ const Login = () => {
login()
}

const oidcLogin = (e) => {
e.preventDefault()
window.location =
`${OIDC_PROVIDER_URL}?state=${OIDC_STATE}&` + // eslint-disable-line
`client_id=${OIDC_CLIENT_ID}&` + // eslint-disable-line
`redirect_uri=${OIDC_REDIRECT_URI}&` + // eslint-disable-line
`response_type=${OIDC_RESPONSE_TYPE}&` + // eslint-disable-line
`scope=${OIDC_SCOPE}` // eslint-disable-line
}

if (noRights === true) return <NoRights />

return (
return code ? (
<Grid className={classes.oidcConnexionProgress}>
<Typography variant="h2" color="primary">
Connexion...
</Typography>
<CircularProgress />
</Grid>
) : authCode ? (
<Welcome />
) : (
<>
<Grid container component="main" className={classes.root}>
<Grid item xs={false} sm={6} md={6} className={classes.image} />
Expand Down Expand Up @@ -332,6 +378,17 @@ const Login = () => {
>
{loading ? <CircularProgress /> : 'Connexion'}
</Button>
<Button
type="submit"
onClick={oidcLogin}
variant="contained"
className={clsx(classes.submit, classes.oidcButton)}
style={{ marginBottom: 40 }}
id="oidc-login"
startIcon={<Keycloak height="25px" />}
>
Connexion via Keycloak
</Button>
</Grid>
</Grid>
{/* <Grid container justifyContent="center"> */}
Expand Down
23 changes: 17 additions & 6 deletions src/views/Login/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
marginBottom: theme.spacing(2)
},
bienvenue: {
fontSize: '15px'
fontSize: 15
},
image: {
backgroundImage: `url(${BackgroundLogin})`,
Expand All @@ -22,15 +22,26 @@ const useStyles = makeStyles()((theme: Theme) => ({
backgroundPosition: 'center'
},
submit: {
margin: theme.spacing(2, 0, 5),
margin: theme.spacing(2, 0, 0),
backgroundColor: '#5BC5F2',
color: 'white',
height: '50px',
width: '185px',
borderRadius: '25px'
height: 50,
width: 185,
borderRadius: 25
},
oidcButton: {
backgroundColor: '#153D8A',
width: 250
},
mention: {
marginTop: '8px'
marginTop: 8
},
oidcConnexionProgress: {
margin: '10%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
rowGap: 30
}
}))

Expand Down

0 comments on commit 0a11361

Please sign in to comment.