{"id":1581,"date":"2021-10-05T15:28:00","date_gmt":"2021-10-05T14:28:00","guid":{"rendered":"https:\/\/wp.qongzi.dev\/?p=1581"},"modified":"2023-12-21T15:30:29","modified_gmt":"2023-12-21T14:30:29","slug":"tuto-oauth2-0-via-keycloak-pour-une-application-nuxtjs-et-son-api","status":"publish","type":"post","link":"https:\/\/qongzi.com\/tuto-oauth2-0-via-keycloak-pour-une-application-nuxtjs-et-son-api\/","title":{"rendered":"[Tuto] oAuth2.0 via Keycloak pour une application NuxtJS et son API"},"content":{"rendered":"\n
Nous avons une application web en interface avec une API. L’objectif est de mettre en place une authentification c\u00f4t\u00e9 application web suivant le workflow oAuth2.0 de type Authorization Code<\/em> tout en restreignant l\u2019acc\u00e8s \u00e0 l’API gr\u00e2ce aux jetons g\u00e9n\u00e9r\u00e9s par le serveur d’authentification.<\/p>\n\n\n\n L’application web<\/strong> est bas\u00e9e sur le framework NuxtJS<\/a><\/strong> 2<\/strong><\/a>, nous utilisons le module Nuxt\/auth<\/a> <\/strong>dans sa version next pour g\u00e9rer le flow d’authentification.<\/p>\n\n\n\n L’API est en NodeJS<\/strong> et utilise le framework KoaJS<\/a><\/strong>, le petit fr\u00e8re d’ExpressJS<\/a>. Nous utilisons Typescript pour profiter un maximum des fonctionnalit\u00e9s de l’ORM Prisma<\/a> pour l’interaction avec la base de donn\u00e9es en PostgreSQL<\/a>. Les extraits de code comprendront donc cette couche mais vous pourrez tout \u00e0 fait les r\u00e9utiliser sans Typescript et avec l’ORM\/ODM de votre choix. Vous vous y retrouverez \u00e9galement facilement si votre API est bas\u00e9e sur ExpressJS.<\/p>\n\n\n\n Le serveur d’authentification est Keycloak<\/a><\/strong>, une solution open source de gestion des diff\u00e9rents workflows de gestion d’acc\u00e8s.<\/p>\n\n\n\n Nous souhaitons coller au plus proche de la RFC 6749<\/a> et du type de workflow Authorization Code<\/a>. Cette m\u00e9thode garantit de ne pas manipuler de donn\u00e9es critiques au sein de nos applications via l’utilisation d’un code d\u00e9bloquant la g\u00e9n\u00e9ration du token.<\/p>\n\n\n\n Dans notre cas, il serait tout \u00e0 fait possible de g\u00e9rer le workflow uniquement depuis NuxtJS de fa\u00e7on tr\u00e8s simple gr\u00e2ce au module nuxt\/auth :<\/p>\n\n\n\n \u26a0\ufe0f Pas de panique, nous reviendrons sur les diff\u00e9rentes configurations au gr\u00e9 de l’article !<\/strong><\/p>\n\n\n\n Toutefois, l’enjeu est de s’authentifier c\u00f4t\u00e9 application web pour contr\u00f4ler l’acc\u00e8s \u00e0 notre API. Pour cela nous avons choisi d’utiliser la librairie openid-client<\/a> qui permet d’interagir depuis le backend avec le serveur d’autorisation (Keycloak).<\/p>\n\n\n\n Ce besoin ajoute quelques contraintes :<\/p>\n\n\n\n Il est donc n\u00e9cessaire de g\u00e9rer l’ensemble du workflow via l’API qui va elle instancier un client openid avec le secret de fa\u00e7on s\u00e9curis\u00e9e.<\/p>\n\n\n\n Nous nous retrouvons avec ce type de workflow :<\/p>\n\n\n\n Nous vous pr\u00e9sentons une configuration pour tester en local avec l’application NuxtJS et l’API tournant respectivement sur les ports 3000 et 1337 (sur http:\/\/localhost) et un conteneur Docker pour Keycloak expos\u00e9 sur le port 8080.<\/p>\n\n\n\n Commencez par cr\u00e9er le conteneur Keycloak :<\/p>\n\n\n\n \u00c9tant donn\u00e9 que nous sommes en local, nous utilisons un acc\u00e8s peu s\u00e9curis\u00e9 avec l’utilisateur et le mot de passe admin. <\/em>Pour le reste de la commande :<\/p>\n\n\n\n Une fois le conteneur lanc\u00e9, vous pouvez y acc\u00e9der depuis votre navigateur : http:\/\/localhost:8080\/auth\/<\/a><\/p>\n\n\n\n Pour vous connecter \u00e0 l’interface administrateur, cliquez sur la tuile Administration Console et rentrez vos identifiants.<\/p>\n\n\n\n Je ne vais ensuite pas d\u00e9tailler la configuration du realm et du client, car tout cela est d\u00e9j\u00e0 tr\u00e8s bien d\u00e9taill\u00e9 dans la documentation officielle<\/a>.<\/p>\n\n\n\n \u26a0\ufe0f Dans la configuration de votre client, n’oubliez pas de mettre le type d’acc\u00e8s en confidentiel<\/strong><\/p>\n\n\n\n Nous n’allons pas reprendre toute la configuration d’une API NodeJS\/KoaJS ici, les extraits de code suivant partent du principe que :<\/p>\n\n\n\n Pour ma part nous avons un objet config<\/em> contenant toutes les variables d’environnement n\u00e9cessaires au fonctionnement de l’API, sa configuration se fait dans le fichier env.ts<\/em> du dossier config<\/em> :<\/p>\n\n\n\n 2.2.1. Initialiser le client openid<\/strong><\/p>\n\n\n\n Le client openid est configur\u00e9 dans le fichier openId.ts<\/em> du dossier config <\/em>:<\/p>\n\n\n\n L’initialisation se fait au lancement du serveur, de notre c\u00f4t\u00e9 nous le faisons dans une IIFE :<\/p>\n\n\n\n 2.2.2. Cr\u00e9ation des routes<\/strong><\/p>\n\n\n\n Vous adapterez la logique \u00e0 votre architecture. Pour notre exemple nous avons les routes et les contr\u00f4leurs dans un dossier api\/namespace, <\/em>pour l’authentification nous travaillons dans api\/auth :<\/em><\/p>\n\n\n\n Nos routes sont simplement configur\u00e9es dans le fichier routes.ts, un middleware ajoutant toutes les routes de l’API est impl\u00e9ment\u00e9 dans le param\u00e9trage de l’application.<\/p>\n\n\n\n Pour notre workflow d’authentification nous avons besoin de trois routes : auth<\/strong> (GET \/auth)<\/em> pour la redirection vers le formulaire du serveur Keycloak, token <\/strong>(POST \/auth\/token)<\/em> pour la g\u00e9n\u00e9ration du token et son rafraichissement, me <\/strong>(GET \/auth\/me)<\/em> pour r\u00e9cup\u00e9rer les informations utilisateurs :<\/p>\n\n\n\n Et le typage du contr\u00f4leur :<\/p>\n\n\n\n 2.2.4. Redirection vers le formulaire d’authentification<\/strong><\/p>\n\n\n\n Le r\u00f4le de ce contr\u00f4leur (de la route \/auth<\/em>) est de r\u00e9ceptionner la requ\u00eate provenant de l’application web au clic sur le bouton “Se connecter” pour rediriger le client vers la page d’authentification du serveur Keycloak.<\/p>\n\n\n\n La requ\u00eate en GET fourni des param\u00e8tres n\u00e9cessaires a la redirection :<\/p>\n\n\n\n Suite \u00e0 quoi le client est redirig\u00e9 vers la page d’authentification Keycloak. L’\u00e9tape suivante se fait donc \u00e0 la validation du formulaire qui va renvoyer vers l’application web qui va elle directement requ\u00eater la route (configur\u00e9e dans nuxt.config.js comme nous le verrons apr\u00e8s) pour r\u00e9cup\u00e9rer le jeton gr\u00e2ce \u00e0 l’authorization code \/auth\/token.<\/em><\/p>\n\n\n\n 2.2.5. G\u00e9n\u00e9ration du token<\/strong><\/p>\n\n\n\n Nous allons commencer par pr\u00e9senter le contr\u00f4leur dans sa version la plus simple, sans la gestion du refresh token. Nous avons fait le choix de s\u00e9parer la logique li\u00e9e au client openid dans un fichier outil pour clarifier le code dans le contr\u00f4leur.<\/p>\n\n\n\n La logique dans ce contr\u00f4leur est la suivante :<\/p>\n\n\n\n \u26a0\ufe0f Les nombreuses conditions font office de type guards \u00e9tant donn\u00e9 que Typescript ne consid\u00e8re pas le ctx.throw comme une fin de script<\/strong><\/p>\n\n\n\n Les outils utilis\u00e9s ici sont simples :<\/p>\n\n\n\n Le set de token est renvoy\u00e9 \u00e0 l’application web, comprenant l’access token<\/em> et le refresh token<\/em>. La suite du workflow est la r\u00e9cup\u00e9ration des donn\u00e9es utilisateurs sur la route \/auth\/me <\/em>configur\u00e9e dans le nuxt.config.js.<\/p>\n\n\n\n 2.2.6. R\u00e9cup\u00e9ration des informations utilisateurs<\/strong><\/p>\n\n\n\n Cette route est prot\u00e9g\u00e9e, nous allons voir apr\u00e8s comment assurer la protection de l’API. L’acc\u00e8s est param\u00e9tr\u00e9 selon la pr\u00e9sence d’un token Keycloak valide ou non sur l’ensemble des routes de l’API, except\u00e9 les routes \/auth<\/em> et \/auth\/token<\/em> d\u00e9crites plus haut.<\/p>\n\n\n\n Le contr\u00f4leur a pour r\u00f4le de retrouver l’utilisateur correspondant au token en base de donn\u00e9es pour retourner le profil utilisateur qui sera stock\u00e9 dans le state de notre application dans la variable $auth.user.<\/em><\/p>\n\n\n\n \u26a0\ufe0f On passe par le middleware <\/strong>openid<\/strong><\/em> sur cette route, son r\u00f4le est de checker la validit\u00e9 du token et de peupler la propri\u00e9t\u00e9 state du context Koa avec les donn\u00e9es utilisateurs<\/strong><\/p>\n\n\n\n 2.2.7. Le middleware openid<\/strong><\/p>\n\n\n\n Il est le gardien de l’API, toutes les requ\u00eates passent par lui. Son r\u00f4le est de :<\/p>\n\n\n\n On va ensuite l’importer et l’initialiser dans la chaine de middleware de l’API, nous avons pour \u00e7a un fichier app.ts<\/em> \u00e0 la racine qui exporte la fonction initApp utilis\u00e9e dans le fichier server.ts<\/em> :<\/p>\n\n\n\n Pour le moment on en a termin\u00e9 avec la configuration c\u00f4t\u00e9 API, le gros morceau est pass\u00e9 car la configuration c\u00f4t\u00e9 NuxtJS est tr\u00e8s simple gr\u00e2ce au module nuxt\/auth !<\/p>\n\n\n\n La premi\u00e8re \u00e9tape est d’installer le module, la documentation recommande<\/a> d’utiliser une version pr\u00e9cise pour :<\/p>\n\n\n\n \u26a0\ufe0f Il est n\u00e9cessaire d’avoir install\u00e9 le module @nuxtjs\/axios \u00e9galement<\/strong><\/em><\/p>\n\n\n\n Ensuite dans le fichier nuxt.config.js il faut ajouter le module \u00e0 la liste de modules :<\/p>\n\n\n\n Puis configurer le module via la propri\u00e9t\u00e9 auth<\/em> dans le m\u00eame fichier<\/p>\n\n\n\n Cette configuration passe principalement par la d\u00e9finition de strat\u00e9gies, nous allons d\u00e9finir une strat\u00e9gie nomm\u00e9e keycloak :<\/p>\n\n\n\n 3.1. Le scheme custom<\/strong><\/p>\n\n\n\n Vous avez remarqu\u00e9 ? Le scheme est custom :<\/p>\n\n\n\n Il s’agit d’une extension du scheme oAuth2 du module. Cette extension ne devrait pas \u00eatre n\u00e9cessaire mais \u00e0 l’heure actuelle il existe une issue ouverte \u00e0 cause d’un probl\u00e8me de redirection au logout<\/em>. Cet override de la m\u00e9thode logout<\/em> est donc un fix temporaire. Rien n’entrave le flow par d\u00e9faut il s’agit simplement d’un fix de la redirection. \u00c0 terme, on pourra utiliser le scheme oAuth2 \u00e0 la place.<\/p>\n\n\n\n 3.2. Les endpoints<\/strong><\/p>\n\n\n\n Les endpoints ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment d\u00e9crite dans cet article car l’API va faire office de proxy entre notre application web et keycloak. Ainsi, les routes authorization, token et userInfo correspondent aux trois routes c\u00f4t\u00e9 API. Le logout ne n\u00e9cessite pas de proxy car une fois la session keycloak r\u00e9voqu\u00e9e, l’utilisateur est d\u00e9connect\u00e9 de l’application web et les jetons stock\u00e9s sont r\u00e9voqu\u00e9s et ne passeront donc plus le middleware openid de l’API.<\/p>\n\n\n\n API_URL<\/em><\/strong> correspond \u00e0 l’URL de base de l’API (e.g. http:\/\/localhost:1337<\/em>).KEYCLOAK_BASE_URL<\/strong> prend cette structure :<\/p>\n\n\n\n 3.3. Les autres param\u00e8tres<\/strong><\/p>\n\n\n\n Je vous laisse vous r\u00e9f\u00e9rerauxr\u00e9f\u00e9rencesduscheme<\/a> pour les autres param\u00e8tres. Il faut juste obligatoirement renseign\u00e9 le clientId que vous trouverez dans Keycloak et la propri\u00e9t\u00e9 scope qui est configur\u00e9 comme cela :<\/p>\n\n\n\nStack<\/h2>\n\n\n\n
1. Le workflow<\/h2>\n\n\n\n
1.1. Le standard<\/h3>\n\n\n\n
1.2. Notre cas<\/h3>\n\n\n\n
\/\/ nuxt.config.js\r\n\r\nconst KEYCLOAK_BASE_URL = `${VOTRE_HOST_KEYCLOAK}\/auth\/realms\/${VOTRE_REALM_KEYCLOAK}\/protocol\/openid-connect`;\r\n\r\n\r\nexport default {\r\n \/\/ Votre config nuxt...\r\n \r\n modules: [\r\n \t\/\/ vos modules...,\r\n '@nuxtjs\/auth-next',\r\n ],\r\n \r\n auth: {\r\n strategies: {\r\n keycloak: {\r\n scheme: 'oauth2', \r\n endpoints: {\r\n authorization: `${KEYCLOACK_BASE_URL}\/auth`,\r\n token: `${KEYCLOACK_BASE_URL}\/token`,\r\n userInfo: `${KEYCLOACK_BASE_URL}\/auth\/me`,\r\n logout: `${KEYCLOACK_BASE_URL}\/logout`,\r\n },\r\n token: {\r\n property: 'access_token',\r\n type: 'Bearer',\r\n name: 'Authorization',\r\n maxAge: ACCESS_TOKEN_MAX_AGE || 15,\r\n },\r\n refreshToken: {\r\n property: 'refresh_token',\r\n maxAge: REFRESH_TOKEN_MAX_AGE || 60 * 60 * 24 * 30,\r\n },\r\n responseType: 'code',\r\n grantType: 'authorization_code',\r\n clientId: VOTRE_KEYCLOAK_CLIENT_ID,\r\n scope: ['openid', 'profile', 'email'],\r\n codeChallengeMethod: 'S256',\r\n \t},\r\n\t\t},\r\n \t},\r\n}\n<\/code><\/pre>\n\n\n\n
\n
2. Configuration<\/h2>\n\n\n\n
2.1. Keycloak<\/h3>\n\n\n\n
docker run --name keycloack -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -d quay.io\/keycloak\/keycloak:15.0.2\\<\/code><\/pre>\n\n\n\n
\n
2.2. L’ API<\/h3>\n\n\n\n
\n
import { config as loadDotEnv } from 'dotenv';\r\n\r\nloadDotEnv({ path: '.env' }); \/\/ A mettre \u00e0 jour selon votre chemin vers le fichier .env\r\n\r\nexport interface Config {\r\n version: string;\r\n port: number;\r\n host: string;\r\n logLevel: string;\r\n keycloakHost: string;\r\n keycloakRealm: string;\r\n keycloakClientId: string;\r\n keycloakClientSecret: string;\r\n}\r\n\r\nconst {\r\n VERSION,\r\n PORT,\r\n HOST,\r\n LOG_LEVEL,\r\n KEYCLOAK_HOST,\r\n KEYCLOAK_REALM,\r\n KEYCLOAK_CLIENT_ID,\r\n KEYCLOAK_CLIENT_SECRET,\r\n} = process.env;\r\n\r\nconst config: Config = {\r\n version: VERSION || 'Dummy version',\r\n port: +(PORT || 1337),\r\n host: HOST || 'http:\/\/localhost',\r\n logLevel: LOG_LEVEL || 'info',\r\n keycloakHost: KEYCLOAK_HOST || 'http:\/\/localhost:8080',\r\n keycloakRealm: KEYCLOAK_REALM || 'master',\r\n keycloakClientId: KEYCLOAK_CLIENT_ID || 'some dummy client id',\r\n keycloakClientSecret: KEYCLOAK_CLIENT_SECRET || 'some dummy secret',\r\n};\r\n\r\nexport default config;<\/code><\/pre>\n\n\n\n
import { Client, Issuer } from 'openid-client';\r\n\r\n\/\/ Nous utilisons des alias pour les chemins relatifs a l'API\r\nimport config from '@config\/env';\r\n\r\n\/\/ Cette constante sera r\u00e9utilis\u00e9e dans un contr\u00f4leur\r\nexport const OPEN_ID_REALM_URL = `${config.keycloakHost}\/auth\/realms\/${config.keycloakRealm}`;\r\n\r\n\/\/ On initialise la variable qui sera r\u00e9assign\u00e9e au lancement du serveur\r\nlet issuer: Issuer<Client>;\r\n\r\n\/\/ Fonction qui initialise l'issuer au lancement du serveur\r\nconst initOpenIdIssuer = async (): Promise<void> => {\r\n issuer = await Issuer.discover(OPEN_ID_REALM_URL);\r\n};\r\n\r\n\/\/ Fonction qui permet de r\u00e9cup\u00e9rer le client openid parametr\u00e9 avec l'id et le secret dans l'API\r\nexport const getOpenIdClient = (): Client =>\r\n new issuer.Client({\r\n client_id: config.keycloakClientId,\r\n client_secret: config.keycloakClientSecret,\r\n });\r\n\r\nexport default initOpenIdIssuer;<\/code><\/pre>\n\n\n\n
import { createServer } from 'http';\r\n\r\nimport initApp from '@config\/app';\r\nimport config from '@config\/env';\r\nimport initOpenIdIssuer from '@config\/openId';\r\n\r\n(async () => {\r\n \/\/ Initialisation de l'issuer\r\n await initOpenIdIssuer();\r\n\r\n \/\/ Initialisation de l'application KoaJS et du serveur HTTP\r\n const app = initApp();\r\n const server = createServer(app.callback());\r\n\r\n \/\/ Lancement du serveur\r\n server.listen(config.port);\r\n})();<\/code><\/pre>\n\n\n\n
src\/api\/auth\r\n \u251c\u2500\u2500 controllers\r\n \u2502 \u2514\u2500\u2500 index.ts\r\n \u251c\u2500\u2500 routes.ts\r\n \u2514\u2500\u2500 tools\r\n \u2514\u2500\u2500 index.ts\r\n<\/code><\/pre>\n\n\n\n
import { Middleware } from 'koa';\r\n\r\nimport controllers from '@api\/auth\/controllers';\r\n\r\ninterface Route {\r\n method: 'get' | 'post' | 'patch' | 'delete';\r\n path: string;\r\n handler: Middleware;\r\n middlewares?: Middleware[];\r\n}\r\n\r\ninterface RouteDefinition {\r\n prefix: string;\r\n routes: Route[];\r\n}\r\n\r\nconst authRoutes: RouteDefinition = {\r\n prefix: '\/auth',\r\n routes: [\r\n {\r\n method: 'get',\r\n path: '\/',\r\n handler: controllers.redirectToOpenIdAuth,\r\n },\r\n {\r\n method: 'post',\r\n path: '\/token',\r\n handler: controllers.generateToken,\r\n },\r\n {\r\n method: 'get',\r\n path: '\/me',\r\n handler: controllers.me,\r\n },\r\n ],\r\n};\r\n\r\nexport default authRoutes;\r\n<\/code><\/pre>\n\n\n\n
import { Middleware } from 'koa';\r\n\r\ninterface AuthController {\r\n redirectToOpenIdAuth: Middleware;\r\n generateToken: Middleware;\r\n me: Middleware;\r\n}<\/code><\/pre>\n\n\n\n
\n
async redirectToOpenIdAuth(ctx) {\r\n const neededParams = ['client_id', 'response_type', 'state', 'redirect_uri'];\r\n\r\n\t\/\/ Reduce qui permet le formattage et le filtrage des query params\r\n const queryParams = Object.entries(ctx.request.query).reduce(\r\n (acc, [key, value]) => (neededParams.includes(key) ? `${acc}&${key}=${value}` : acc),\r\n ''\r\n );\r\n\r\n\t\/\/ URI de redirection basee sur la variable definie precedemment\r\n const redirectUri = `${OPEN_ID_REALM_URL}\/protocol\/openid-connect\/auth?${queryParams}`;\r\n\r\n\t\/\/ Redirection en reponse\r\n ctx.redirect(redirectUri);\r\n},<\/code><\/pre>\n\n\n\n
\n
async generateToken(ctx) {\r\n const { body } = ctx.request;\r\n const { grant_type } = body;\r\n\r\n\t\/\/ Initialisation du tokenSet\r\n let tokenSet: TokenSet | null = null;\r\n\r\n\t\/\/ Generation du token si le grant type est bon\r\n if (grant_type === 'authorization_code') {\r\n tokenSet = await generateToken(body);\r\n }\r\n\r\n\t\/\/ Levee d'une exception si pas de token\r\n if (!tokenSet || !tokenSet.access_token) {\r\n \r\n \/\/ Revocation du token au cas ou il manque l'access token dans le set\r\n if (tokenSet) {\r\n await revokeTokenSet(tokenSet);\r\n }\r\n ctx.throw(400);\r\n } else {\r\n \r\n \/\/ Validation du token pour voir si l'utilisateur existe aussi dans la base de donnees metier\r\n const isTokenValid = await validateToken(tokenSet.access_token);\r\n\r\n\r\n\t \/\/ Si l'utilisateur existe en base, on renvoie le token, sinon on leve une exception apres avoir revoke le set de token\r\n if (isTokenValid) {\r\n ctx.body = tokenSet;\r\n } else {\r\n await revokeTokenSet(tokenSet);\r\n ctx.throw(400);\r\n }\r\n }\r\n },<\/code><\/pre>\n\n\n\n
type GenerateToken = (args: {\r\n code: string;\r\n redirect_uri: string;\r\n grant_type: 'authorization_code';\r\n}) => Promise<TokenSet>;\r\n\r\nexport const generateToken: GenerateToken = ({ code, redirect_uri, grant_type }) =>\r\n getOpenIdClient().grant({\r\n grant_type,\r\n code,\r\n redirect_uri,\r\n });<\/code><\/pre>\n\n\n\n
type ValidateToken = (token: string) => Promise<boolean>;\r\n\r\nexport const validateToken: ValidateToken = async (token) => {\r\n const decodedToken = decode(token);\r\n\r\n if (!decodedToken || typeof decodedToken === 'string') {\r\n return false;\r\n }\r\n\r\n const { email } = decodedToken;\r\n const user = await userService.findUnique({ where: { email } });\r\n\r\n return !!user;\r\n};<\/code><\/pre>\n\n\n\n
type RevokeTokenSet = (tokenSet: TokenSet) => Promise<void>;\r\n\r\nexport const revokeTokenSet: RevokeTokenSet = async (tokenSet: TokenSet) => {\r\n const { access_token, refresh_token } = tokenSet;\r\n const openIdClient = getOpenIdClient();\r\n if (access_token) {\r\n await openIdClient.revoke(access_token);\r\n }\r\n\r\n if (refresh_token) {\r\n await openIdClient.revoke(refresh_token);\r\n }\r\n};<\/code><\/pre>\n\n\n\n
async me(ctx) {\r\n \t\/\/ On recupere l'utilisateur stock\u00e9 dans le state par le middleware\r\n const { user } = ctx.state;\r\n\r\n\t\/\/ Si pas d'utilisateur on l\u00e8ve une exception\r\n if (!user) {\r\n ctx.throw(404, 'Not found');\r\n }\r\n\r\n\t\/\/ Sinon on renvoie l'utilisateur en r\u00e9ponse\r\n ctx.body = user;\r\n },<\/code><\/pre>\n\n\n\n
\n
import { Middleware } from 'koa';\r\n\r\nimport { getOpenIdClient } from '@config\/openId';\r\nimport userService from '@api\/user\/service';\r\n\r\nconst openIdMiddleware: () => Middleware = () => async (ctx, next) => {\r\n \/\/ On recupere le header authorization contenant le token et l'URL sans les query parametres\r\n const { authorization } = ctx.headers;\r\n const [baseUrl] = ctx.url.split('?');\r\n\r\n \/\/ On check si la route est whiteliste ou non\r\n if (!isRouteAuthorized(baseUrl)) {\r\n \r\n \/\/ Si ce n'est pas le cas et qu'il n'y a pas de token, on leve une exception 403\r\n if (!authorization) {\r\n ctx.throw(403, 'Unauthorized');\r\n } else {\r\n \r\n \/\/ Sinon on recupere le token, on verifie sa validite et on recupere l'email utilisateur\r\n const token = getToken(authorization);\r\n const { active, email } = await getOpenIdClient().introspect(token, 'access_token');\r\n\r\n\t \/\/ Si le token n'est pas actif ou si le mail n'existe pas, on leve une 403 \r\n if (!active || !email || typeof email !== 'string') {\r\n ctx.throw(403, 'Unauthorized');\r\n } else {\r\n \r\n \t\/\/ Sinon on verifie que l'utilisateur soit bien existant cote metier\r\n const user = await userService.findUnique({ where: { email }, include: { roles: true } });\r\n\r\n\t\t\/\/ Si ce n'est pas le cas, on leve une 403, sinon on complete le state du contexte avec l'utilisateur\r\n if (!user) {\r\n ctx.throw(403, 'Unauthorized');\r\n } else {\r\n ctx.state.user = user;\r\n }\r\n }\r\n }\r\n }\r\n\r\n \/\/ Standard pour un middleware KoaJS\r\n await next();\r\n};\r\n\r\n\/\/ Les routes whitelistees\r\nconst isRouteAuthorized = (url: string): RegExpMatchArray | null =>\r\n url.match(\/^\\\/auth$\/) || url.match(\/^\\\/auth\\\/token$\/);\r\n\r\nconst getToken = (authorizationHeader: string): string => authorizationHeader.split(' ')[1];\r\n\r\nexport default openIdMiddleware;<\/code><\/pre>\n\n\n\n
const initApp = (): Koa => {\r\n const app = new Koa();\r\n\r\n app.use(logger({ level: config.logLevel }));\r\n app.use(helmet());\r\n \r\n \/\/ Le middleware openid est apres helmet et avant les cors, cela evite d'aller trop loin dans la chaine si l'acces est refuse\r\n app.use(openIdMiddleware());\r\n \r\n app.use(cors());\r\n app.use(bodyParser());\r\n\r\n app.use(router.routes());\r\n app.use(router.allowedMethods());\r\n\r\n return app;\r\n};<\/code><\/pre>\n\n\n\n
3. NuxtJS avec nuxt\/auth<\/h3>\n\n\n\n
npm install --save-exact @nuxtjs\/auth-next<\/code><\/pre>\n\n\n\n
\/\/ nuxt.config.js\r\n\r\nmodules: [\r\n\t'@nuxtjs\/axios',\r\n\t'@nuxtjs\/auth-next',\r\n],<\/code><\/pre>\n\n\n\n
\/\/ nuxt.config.js\r\n\r\nauth: {},<\/code><\/pre>\n\n\n\n
auth: {\r\n strategies: {\r\n keycloak: {\r\n scheme: '~\/config\/authScheme',\r\n },\r\n },\r\n },<\/code><\/pre>\n\n\n\n
import { Oauth2Scheme } from '@nuxtjs\/auth-next\/dist\/runtime';\r\n\r\nfunction encodeQuery(queryObject) {\r\n return Object.entries(queryObject)\r\n .reduce((acc, [key, value]) => {\r\n if (typeof value !== 'undefined') {\r\n acc.push(encodeURIComponent(key) + (value !== null ? '=' + encodeURIComponent(value) : ''));\r\n }\r\n\r\n return acc;\r\n }, [])\r\n .join('&');\r\n}\r\n\r\nexport default class KeycloakScheme extends Oauth2Scheme {\r\n logout() {\r\n if (this.options.endpoints.logout) {\r\n const opts = {\r\n client_id: this.options.clientId + '',\r\n redirect_uri: this.logoutRedirectURI,\r\n };\r\n const url = this.options.endpoints.logout + '?' + encodeQuery(opts);\r\n window.location.replace(url);\r\n }\r\n\r\n return this.$auth.reset();\r\n }\r\n}\r\n<\/code><\/pre>\n\n\n\n
auth: {\r\n strategies: {\r\n keycloak: {\r\n scheme: '~\/config\/authScheme',\r\n },\r\n endpoints: {\r\n authorization: `${API_URL}\/auth`,\r\n token: `${API_URL}\/auth\/token`,\r\n userInfo: `${API_URL}\/auth\/me`,\r\n logout: `${KEYCLOACK_BASE_URL}\/logout`,\r\n },\r\n },\r\n },<\/code><\/pre>\n\n\n\n
const KEYCLOACK_BASE_URL = `${process.env.KEYCLOAK_REMOTE_HOST}\/auth\/realms\/${process.env.KEYCLOAK_REALM}\/protocol\/openid-connect`;<\/code><\/pre>\n\n\n\n
scope: ['openid', 'profile', 'email'],\r\n<\/code><\/pre>\n\n\n\n