Le blog

Un pas vers la décentralisation, reprenons le contrôle grâce à OIDC !

Publié le 21 septembre 2023

Grâce au Linked Data, il nous faut penser nos applications différemment, avant tout de manière plus sécurisée ! Fini le temps où l’on se connecte indépendamment sur chaque site, où l’on gère un mot de passe différent par site, où nos données transitent de manière non sécurisée, il nous faut une authentification pleinement sécurisée… et décentralisée ! Pour ça, il nous faut répondre aux problématiques suivantes :

  • authentification unique : améliorer l’expérience des utilisateurs, ceux-ci ne devraient avoir à s’authentifier qu’une seule fois pour l’ensemble des applications du service (Single Sign On) ;
  • authentification forte : identifier précisément à qui on a affaire, au-delà d’un simple username, pour les applications nécessitant une sécurité renforcée ;
  • authentification configurable : chaque client doit pouvoir être configuré séparément, afin de lui définir un niveau de sécurité et de permission unique, et une méthode d’authentification adaptée.

Ça tombe bien, il existe un protocole qui gère la plupart de ces besoins : OAuth (Open Authorization). Mais lui seul ne suffit pas dans notre cas, il nous faut le compléter grâce à OIDC (OpenID Connect).

Quels sont ces protocoles ? Qu’est-ce qui les différencie ?

Disclaimer #

Sécuriser son application derrière une authentification, c’est bien. Choisir le bon système d’authentification, c’est mieux ! Avant de partir sur OpenID Connect, posez-vous bien la question : “En ai-je vraiment besoin ?”.

Si votre système d’information n’est composé que d’un seul service, la réponse est probablement non. L’authentification mise en place dans votre seul service répondra aux problématiques citées en introduction : une authentification unique, forte et configurable.

Cependant, si votre système d’information est composé de plusieurs services (par exemple : une PWA et une API), alors la réponse est probablement oui.

Rappelez-vous bien ceci : rien ne sert de mettre en place un système complexe sur une application relativement simple. Cela reviendrait à utiliser une enclume pour enfoncer un clou : c’est lourd et inadapté.

Aux origines d’OpenID Connect #

En mai 2005, Brad Fitzpatrick crée le protocole OpenID Authentication (initialement nommé Yadis : Yet Another Distributed Identity System). Ce protocole a rapidement pris de l’ampleur, notamment grâce aux contributions de ZDNet, Symantec, Microsoft, AOL et Sun Microsystems. En mai 2007, l’OpenID Foundation est alors créée avec tout ce beau monde, rejointe rapidement par Yahoo!, Google, IBM, VeriSign et SourceForge, Inc.

De son côté, OAuth 1.0 (RFC5849) est publié en avril 2010, et met l’accent sur l’autorisation. OAuth 2.0 (RFC6749) sort deux ans plus tard en octobre 2012.

Enfin, l’OpenID Foundation publie en février 2014 OIDC 1.0, un protocole d’identification qui complète OAuth 2.0.Et depuis ? OAuth 2.0 a évolué grâce à de multiples RFC (notamment RFC8252 OAuth 2.0 for Native Apps, et RFC7636 Proof Key for Code Exchange). Depuis juillet 2020, OAuth 2.1 est en cours d’étude afin de simplifier OAuth 2.0.

“Identification”, “authentification”, “autorisation”… Ces mots apparaissent souvent mais sont hélas souvent confondus. Pourtant, ils ont une distinction très importante dans le sujet que nous abordons aujourd’hui. Mettons les choses au clair avant d’aller plus loin !

Identification vs authentification vs autorisation #

L’identification répond à la question “Qui êtes-vous ?”. Elle consiste à donner une information déclarant une identité. Cette dernière peut être définie comme étant la somme de toutes les caractéristiques qui font de cette personne qui elle est : son nom, sa date de naissance, son lieu de résidence, son numéro de carte d’identité (CNI) ou de carte vitale (NIR), etc. Ces caractéristiques sont appelées attributs d'identité. Certains attributs d’identité peuvent être communs (plusieurs personnes peuvent avoir la même date de naissance ou lieu de résidence), mais la somme de ces attributs est unique et constitue l’identité d’une seule personne ou application.

Cependant, si ma réponse est “Je suis Vincent CHALAMON”, rien ne prouve que ce soit vrai. Fournir un identifiant unique n’est pas suffisant, il faut pouvoir prouver son identité. L’authentification intervient après l’identification, et vérifie cette déclaration (grâce à un mot de passe, par exemple) en répondant à la question : “Êtes-vous réellement Vincent CHALAMON ?”. On parle alors d’authentification simple. Si je devais saisir un élément d’authentification supplémentaire (un code temporaire reçu sur mon téléphone par exemple), il s'agirait alors d’authentification multifacteur.

L’autorisation arrive en dernière étape de ce processus de sécurité, elle consiste à contrôler les droits d’une personne ou application identifiée et authentifiée. Maintenant que j’ai prouvé qui je suis, l’autorisation répond à la question : “En tant que Vincent CHALAMON, avez-vous le droit d’effectuer telle action ?”.

Une authentification sans identification n’a aucun sens. Il serait inutile de vérifier l’autorisation. L’utilisateur doit d’abord se présenter. De même, il serait dangereux d’avoir une identification sans authentification. En revanche, il est possible d’avoir une autorisation sans identification, ni authentification. Par exemple, une API accessible publiquement autorise tout le monde à lire l’information sans vous identifier ni authentifier.

Comment OAuth et OIDC intègrent-ils ces notions dans leurs protocoles ?

OAuth vs OIDC #

OAuth

OAuth est un protocole libre qui permet d'autoriser un site web, un logiciel ou une application (dite “consommateur”) à utiliser l'API sécurisée d'un autre site web (dit “fournisseur”) pour le compte d'un utilisateur. OAuth n'est pas un protocole d'identification, mais de délégation d'autorisation.

OAuth permet aux utilisateurs de donner, au site ou logiciel consommateur, l'accès à ses informations personnelles qu'il a stockées sur le site fournisseur de services ou de données, ceci tout en protégeant le pseudonyme et le mot de passe des utilisateurs.

" class="wp-image-7883
OpenID Connect (OIDC)

OIDC est une surcouche au protocole OAuth ajoutant le support de l’identification utilisateur. Une première contribution du protocole OIDC au protocole OAuth consiste en la standardisation des données obtenues à la suite d’une authentification. Contrairement à OAuth, qui doit envoyer une requête supplémentaire au serveur d’autorisation pour obtenir les données utilisateurs, OIDC permet de les inclure directement dans l’ID Token grâce au JWT.

" class="wp-image-7884

OIDC apporte également quelques nouveautés en termes de sécurité, telles que l’utilisation de la spécification JWT, et l’authentification forte.

OIDC ne remplace pas OAuth mais le complète. Les fonctionnalités du protocole OAuth sont toujours présentes, mais améliorées grâce à une sécurité renforcée et une décentralisation des données utilisateurs.

Ici, comprenez que le mot décentralisé signifie que vous disposez d’un seul fournisseur de vos informations privées : le serveur d’autorisation. Vous avez le contrôle sur celui-ci, sur les informations qui s’y trouvent et sur qui peut y accéder. Et vous les rendez disponibles à plusieurs services. Un service ne dispose pas directement de vos données et vous devez pouvoir révoquer son autorisation. C’est en ce sens que OpenID Connect est décentralisé : le service que vous désirez utiliser n’a pas centralisé 100% des informations nécessaires pour fonctionner, il doit se les procurer auprès du serveur d’autorisation de votre choix. Le projet SOLID essaie de répondre davantage à ce besoin, mais ce sera sûrement le sujet d’un autre article.

OIDC a introduit plusieurs notions importantes, notamment JWT et l’authentification forte. C’est une vraie valeur ajoutée pour une authentification sécurisée et décentralisée (Single Sign On).

JWT

OIDC suit la spécification JWT (JSON Web Token), qui consiste à stocker les données utilisateurs dans un document JSON signé, optionnellement chiffré, puis encodé en base64 pour être partagé entre les applications concernées. Ce jeton permet alors d’authentifier l’utilisateur de façon sécurisée auprès du fournisseur, tout en respectant la contrainte REST “sans état” (stateless).

Note : bien qu’OIDC suive cette spécification, ce n’est pas forcément le cas d’OAuth. Cependant, son utilisation est recommandée grâce à la spécification JOSE (JavaScript Object Signing and Encryption).

La spécification JOSE permet que ce document JSON soit signé (JWS : JSON Web Signature) et/ou chiffré (JWE : JSON Web Encryption). La signature permet de garantir l’authenticité du document JSON, le chiffrement permet de rendre son contenu confidentiel.

(exemple de JWT non chiffré, encodé en base64, avec une signature vérifiée avec le secret “secret”)

En regardant attentivement, ce jeton est composé de trois parties séparées par un point :

  • le header qui contient les détails de l’algorithme de signature (par exemple : {“alg”: “ES256”, “type”: “JWT”})
  • le payload (aussi appelé claims), qui contient les données utilisateurs, chiffrées ou non
  • la signature qui assure l’authenticité de ce jeton (header + payload + secret signés ensemble selon l’algorithme défini dans le header)

Une fois décodé, le payload du JWT prend la forme suivante :

{
  "iss": "https://server.example.com",
  "sub": "248289761001",
  "aud": "s6BhdRkqt3",
  "nonce": "n-0S6_WzA2Mj",
  "exp": 1311281970,
  "iat": 1311280970,
  "name": "John DOE",
  "given_name": "John",
  "family_name": "DOE",
  "gender": "male",
  "birthdate": "1980-10-31",
  "email": "john.doe@example.com",
  "picture": "https://example.com/john.doe/me.jpg"
}

L’encodage du document JSON en base64 facilite son partage mais ne garantit pas son intégrité. Il est donc possible de le décoder facilement, modifier son contenu et le signer à nouveau en respectant le même algorithme renseigné dans le header. C’est pourquoi un secret est utilisé lors de sa signature, dont seul le serveur d’autorisation a connaissance (et éventuellement le fournisseur). Il sera d’ailleurs recommandé d’utiliser des algorithmes asymétriques tels que RS256.

Authentification multifacteur

On a vu précédemment que l’identification répond à la question “Qui êtes-vous ?” et que  l’authentification vérifie cette déclaration.

Concrètement, si je réponds “Je suis Vincent CHALAMON” et le prouve grâce à ma carte d’identité, mon identité ne peut être certifiée à 100% car cette carte d’identité peut être fausse : il s’agit d’une authentification simple basée sur un seul facteur d’authentification. Pour prouver mon identité, je dois fournir plusieurs facteurs d’authentification afin de constituer une authentification multifacteur, grâce à au moins deux facteurs distincts :

  • un facteur de propriété : carte d’identité, smartphone, etc.
  • un facteur d’inhérence : empreinte, visage, etc.
  • un facteur de connaissance : code PIN, mot de passe, etc.

Certes, ces facteurs peuvent être usurpés, volés ou reproduits. Cependant, cela augmente la complexité de la tâche d’un attaquant lors d’une usurpation d’identité.

Note : en 2021, l’ANSSI distingue l’authentification forte de l’authentification multifacteur : l’authentification forte, dans cette définition, repose sur des mécanismes cryptographiques jugés forts mais pas nécessairement sur plusieurs facteurs d’authentification.

Après la théorie, la pratique ! Voyons maintenant comment mettre en place un serveur OIDC et l’utiliser dans un projet API Platform.

Mettre en place un serveur OIDC #

De prime abord, on serait tenté d’implémenter un bundle OAuth Server dans notre projet API Platform. Cependant, ces bundles ne supportent pas toujours le protocole OIDC. De plus, afin de mettre en place une architecture respectant l’état de l’art, il convient de dissocier le fournisseur du serveur d’autorisation : ce dernier doit être une entité à part entière dans notre architecture.

Il existe des solutions SAAS, par exemple Auth0. Mais dans cet article nous allons plutôt nous intéresser à Keycloak pour son aspect Open Source et parce qu’il est une référence sur le marché.

Un serveur OIDC avec Docker Compose

L’installation de Keycloak se fait très facilement grâce à Docker Compose. Nous avons besoin d’un conteneur Keycloak et d’un autre pour sa base de données :

# docker-compose.yml
version: "3.8"

services:
  keycloak-database:
    image: postgres:15-alpine
    volumes:
      - keycloak_db_data:/var/lib/postgresql/data:rw
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_PASSWORD: '!ChangeMe!'
      POSTGRES_USER: keycloak

  keycloak:
    image: bitnami/keycloak:21-debian-11
    environment:
      KEYCLOAK_DATABASE_HOST: keycloak-database
      KEYCLOAK_DATABASE_NAME: keycloak
      KEYCLOAK_DATABASE_USER: keycloak
      KEYCLOAK_DATABASE_PASSWORD: !ChangeMe!
      KEYCLOAK_ADMIN_USER: admin
      KEYCLOAK_ADMIN_PASSWORD: !ChangeMe!
    depends_on:
      - keycloak-database
    ports:
      - target: 8080
        published: 8080
        protocol: tcp

volumes:
  keycloak_db_data:

Démarrez le projet Docker Compose : docker compose up -d. Keycloak est disponible sur http://localhost:8080.

Mais pour les besoins d’un projet API Platform, nous souhaitons faire tourner Keycloak derrière notre reverse-proxy Caddy. Pour cela, ajoutez une règle dans le fichier Caddyfile, et adaptez un peu la configuration Docker Compose :

# api/docker/caddy/Caddyfile
# ...

# Matches requests for OIDC routes
@oidc expression path('/oidc/*')

route {
    # ...
    reverse_proxy @oidc http://{$OIDC_UPSTREAM}
}
# docker-compose.yml
version: "3.8"

services:
  # ...

  keycloak:
    image: bitnami/keycloak:21-debian-11
    environment:
      # ...
      # Must finish with a trailing slash (https://github.com/bitnami/charts/issues/10885#issuecomment-1414279144)
      KEYCLOAK_HTTP_RELATIVE_PATH: /oidc/
      # https://www.keycloak.org/server/hostname
      KC_HOSTNAME_URL: https://${SERVER_NAME:-localhost}/oidc/

Keycloak est dorénavant disponible sur https://localhost/oidc/.

Note : la variable d’environnement KC_HOSTNAME_URL permet de définir l’url publique de Keycloak peu importe le nom de domaine sur lequel il est appelé. Par exemple, tout appel au conteneur Keycloak via le réseau interne Docker Compose (par exemple : http://keycloak:8080/oidc/) retournera toujours des urls https://localhost/oidc/.

Un serveur OIDC permet de cloisonner la configuration par domaine, autrement dit, des définitions et identifications de clients pouvant discuter avec lui, et réclamer une authentification. Keycloak en propose un par défaut nommé master. Afin de respecter l’état de l’art et d’améliorer la sécurité, nous allons créer un domaine dédié à notre projet dans lequel nous pourrons créer nos différents clients.

Créer et configurer un domaine

Depuis l’interface de Keycloak (https://localhost/oidc/), accédez à la console d’administration (Administration Console) et connectez-vous grâce aux identifiants définis dans la configuration Docker Compose (KEYCLOAK_ADMIN_USER, KEYCLOAK_ADMIN_PASSWORD).

Puis, dans le menu déroulant en haut à gauche, créez un domaine et le nommer demo :

Notre domaine est maintenant prêt, n’hésitez pas à jeter un œil à sa configuration pour découvrir tout ce que Keycloak propose et active par défaut, et créer un thème qui vous ressemble.

Configurer API Platform avec OIDC #

Quel flux d’autorisation choisir pour notre projet ?

Le protocole OAuth propose plusieurs flux d’autorisation, allant de l’autorisation utilisateur à l’autorisation applicative en passant par d’autres flux. Dans le cas d’une PWA (le consommateur) consommant une API (le fournisseur), il nous faut un flux permettant à l’utilisateur de s’authentifier auprès de Keycloak (le serveur d’autorisation) et donner son autorisation pour l’exploitation de ses données : il s’agit du flux Authorization Code.

" class="wp-image-7885

Note : on entend souvent parler du flux Implicit, celui-ci est déprécié pour des questions de sécurité.

Il existe plusieurs façons d’implémenter le flux Authorization Code dans API Platform. Par exemple, il est possible d’implémenter un proxy tel que Caddy Security ou OAuth2 Proxy, ou une API Gateway telle que Kong, Gravitee.io ou 3scale.

Pour notre usage, nous allons plutôt utiliser une solution proposée par Symfony et donc facilement intégrable dans notre projet API Platform. Il existe plusieurs bundles : knpuniversity/oauth2-client-bundle, league/oauth2-client, hwi/oauth-bundle, etc. Cependant, ceux-ci ne proposent pas une couche d’autorisation orientée API. Heureusement, Symfony propose un mécanisme adapté à notre besoin grâce à Florent MORSELLI et Vincent CHALAMON : https://github.com/symfony/symfony/pull/46428, https://github.com/symfony/symfony/pull/48272.

Sécuriser l'API

Par défaut, Symfony propose deux stratégies : envoyer le jeton au serveur OIDC pour validation, ou le décoder et le valider par l’API.

Dans le cas d’un envoi du jeton au serveur d’autorisation, ce dernier va le valider et vérifier qu’il n’a pas été manuellement invalidé par un administrateur. Cette stratégie est la plus sécurisée, cependant elle requiert un appel au serveur d’autorisation à chaque requête HTTP, ce qui dégrade les performances :

# api/config/packages/security.yaml
security:
  firewalls:
    main:
      access_token:
        token_handler:
          # OIDC_SERVER_URL_INTERNAL: https://caddy/oidc/realms/demo
          oidc_user_info: '%env(OIDC_SERVER_URL_INTERNAL)%/protocol/openid-connect/userinfo'

Il est possible de décoder et valider le jeton par le fournisseur sans appeler le serveur d’autorisation. Cette stratégie offre de meilleures performances car elle évite un appel au serveur d’autorisation à chaque requête HTTP. Cependant, elle est aussi moins sécurisée dans le cas spécifique où le jeton serait manuellement invalidé par un administrateur du serveur d’autorisation : le jeton sera toujours considéré comme valide jusqu’à son expiration :

# api/config/packages/security.yaml
security:
    firewalls:
        main:
            pattern: ^/
            access_token:
                token_handler:
                    oidc:
                        # Algorithm used to sign the JWS
                        algorithm: 'ES256'
                        # A JSON-encoded JWK
                        key: '{"kty":"...","k":"..."}'

Note : la spécification OIDC ainsi que Google recommandent de décoder et valider le jeton auprès du fournisseur : https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation, https://developers.google.com/identity/openid-connect/openid-connect?hl=fr#validatinganidtoken.

L’API étant maintenant configurée, nous allons pouvoir créer le client PWA auprès de Keycloak afin d’authentifier ses appels.

Sécuriser la PWA

Une PWA ne peut pas être considérée comme un client de confiance car le code est exécuté sur le navigateur de l’utilisateur, zone potentiellement vulnérable. Il est donc fortement découragé d’y stocker un secret (clientSecret). Swagger l’indique d’ailleurs dans sa documentation :

Pour des questions de sécurité, il faut utiliser une PKCE (prononcer “pixy” : Proof Key for Code Exchange). Il s’agit d’une paire de clés de vérification échangée à plusieurs moments entre la PWA et Keycloak, en complément du flux standard. D’ailleurs, le projet SOLID évoqué plus tôt demandera une clé tripartite pour renforcer la sécurité. Cette clé est le point névralgique de ces flux. En revanche, comme la PWA ne peut plus fournir de clientSecret, elle ne peut plus être identifiée de façon certaine par Keycloak : le client est donc de type public.

Note : à l’origine, PKCE a été implémentée pour renforcer le flux Authorization Code sur les clients publics. Son utilisation renforce la sécurité face aux attaques CSRF, il est donc recommandé de l’implémenter même dans un client confidentiel.

De retour sur l’interface d’administration de notre domaine demo dans Keycloak, cliquez sur le menu Clients dans le menu latéral, puis Create client afin de créer et configurer le client. Saisir un Client ID sans espace ni accent. Il sera renseigné par la suite dans notre PWA :

" class="wp-image-7888

Saisissez une ou plusieurs valeurs dans Valid redirect URIs afin de renseigner quelles URIs sont autorisées lors des redirections effectuées par le flux Authorization Code. Ces URIs permettent à Keycloak de contrôler quel client effectue une demande d’authentification.

Pour configurer la PWA, nous allons utiliser NextAuth.js. Cette librairie Open Source propose de multiples connecteurs pré-configurés pour différents serveurs OIDC, dont Keycloak.

Après avoir installé NextAuth.js, créez le fichier suivant :

// pwa/pages/api/auth/[...nextauth].tsx
import NextAuth from "next-auth"
import KeycloakProvider from "next-auth/providers/keycloak"

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    KeycloakProvider({
      id: 'keycloak',
      clientId: process.env.OIDC_CLIENT_ID,
      issuer: process.env.OIDC_SERVER_URL,
      authorization: {
        // https://authjs.dev/guides/basics/refresh-token-rotation#jwt-strategy
        params: {
          access_type: "offline",
          prompt: "consent",
        },
      },
      // https://github.com/nextauthjs/next-auth/issues/685#issuecomment-785212676
      protection: "pkce",
      // https://github.com/nextauthjs/next-auth/issues/4707
      clientSecret: null,
      client: {
        token_endpoint_auth_method: "none"
      },
    }),
  ],
}

export default NextAuth(authOptions)

Puis, ajouter le SessionProvider sur l’application :

// pwa/pages/_app.tsx
import { SessionProvider } from "next-auth/react"

export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

NextAuth.js propose une intégration simple des protocoles OAuth et OIDC, mais requiert du code supplémentaire pour le rafraîchissement du jeton (cf. https://next-auth.js.org/tutorials/refresh-token-rotation) :

// pwa/pages/api/auth/[...nextauth].tsx
import NextAuth, { AuthOptions, SessionOptions } from "next-auth";
import { type TokenSet } from "next-auth/core/types";
import KeycloakProvider from "next-auth/providers/keycloak";

import { OIDC_CLIENT_ID, OIDC_SERVER_URL } from "../../../config/keycloak";

interface Session extends SessionOptions {
  accessToken: string
  error?: "RefreshAccessTokenError"
}

interface JWT {
  accessToken: string
  expiresAt: number
  refreshToken: string
  error?: "RefreshAccessTokenError"
}

interface Account {
  access_token: string
  expires_in: number
  refresh_token: string
}

export const authOptions: AuthOptions = {
  callbacks: {
    async jwt({ token, account }: { token: JWT, account: Account }): Promise<JWT> {
      if (account) {
        // Save the access token and refresh token in the JWT on the initial login
        return {
          accessToken: account.access_token,
          expiresAt: Math.floor(Date.now() / 1000 + account.expires_in),
          refreshToken: account.refresh_token,
        };
      } else if (Date.now() < token.expiresAt * 1000) {
        // If the access token has not expired yet, return it
        return token;
      } else {
        // If the access token has expired, try to refresh it
        try {
          // todo use .well-known
          const response = await fetch(`${OIDC_SERVER_URL}/protocol/openid-connect/token`, {
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: new URLSearchParams({
              client_id: OIDC_CLIENT_ID,
              grant_type: "refresh_token",
              refresh_token: token.refreshToken,
            }),
            method: "POST",
          });

          const tokens: TokenSet = await response.json();

          if (!response.ok) throw tokens;

          return {
            ...token, // Keep the previous token properties
            accessToken: tokens.access_token,
            expiresAt: Math.floor(Date.now() / 1000 + tokens.expires_at),
            // Fall back to old refresh token, but note that
            // many providers may only allow using a refresh token once.
            refreshToken: tokens.refresh_token ?? token.refreshToken,
          };
        } catch (error) {
          console.error("Error refreshing access token", error);
          // The error property will be used client-side to handle the refresh token error
          return {
            ...token,
            error: "RefreshAccessTokenError" as const
          };
        }
      }
    },
    async session({ session, token }: { session: Session, token: JWT }): Promise<Session> {
      // Save the access token in the Session for API calls
      if (token) {
        session.accessToken = token.accessToken;
        session.error = token.error;
      }

      return session;
    }
  },
  providers: [
    KeycloakProvider({
      id: 'keycloak',
      clientId: OIDC_CLIENT_ID,
      issuer: OIDC_SERVER_URL,
      authorization: {
        // https://authjs.dev/guides/basics/refresh-token-rotation#jwt-strategy
        params: {
          access_type: "offline",
          prompt: "consent",
        },
      },
      // https://github.com/nextauthjs/next-auth/issues/685#issuecomment-785212676
      protection: "pkce",
      // https://github.com/nextauthjs/next-auth/issues/4707
      clientSecret: null,
      client: {
        token_endpoint_auth_method: "none"
      },
    }),
  ],
};

export default NextAuth(authOptions);

Enfin, NextAuth.js propose un Hook React useSession pour vérifier l’authentification d’un utilisateur : https://next-auth.js.org/getting-started/example#frontend---add-react-hook. D'une manière plus générale, il est possible de sécuriser tout ou partie de la PWA grâce à Next.js Middleware + NextAuth.js : https://next-auth.js.org/tutorials/securing-pages-and-api-routes.

API, PWA… Tout notre projet semble sécurisé avec OIDC. Vraiment ? Et la documentation d’API ?

Sécuriser notre documentation d’API #

Par défaut, API Platform propose deux interfaces pour notre documentation OpenAPI : Swagger UI et Redoc. Ce dernier ne propose de fonctionnalité de test de requête depuis le navigateur, uniquement dans sa version SAAS. Nous choisirons donc Swagger UI pour notre démonstration.

Il faut considérer Swagger UI comme un client à part entière, au même titre que la PWA. Swagger UI propose d’ailleurs un support d'OAuth en configurant les options nécessaires.

Créez le client api-platform-swagger dans Keycloak de la même manière que le client api-platform-pwa a été précédemment créé.

API Platform dispose d’une option pour configurer Swagger UI :

# api/.env
OIDC_SERVER_URL=https://localhost/oidc/realms/demo
OIDC_SWAGGER_CLIENT_ID=api-platform-swagger
# api/config/packages/api_platform.yaml
api_platform:
    # …
    oauth:
        enabled: true
        clientId: '%env(OIDC_SWAGGER_CLIENT_ID)%'
        pkce: true
        type: oauth2
        flow: authorizationCode
        tokenUrl: '%env(OIDC_SERVER_URL)%protocol/openid-connect/token'
        authorizationUrl: '%env(OIDC_SERVER_URL)%protocol/openid-connect/auth'

API, PWA…et maintenant Swagger UI. Félicitations ! Notre projet API Platform est pleinement sécurisé avec OIDC !

En résumé #

La sécurité d’une application n’est pas un sujet à prendre à la légère ! Mais grâce à OIDC et son intégration dans API Platform, le projet s'ouvre à une excellente sécurité et les utilisateurs gardent le contrôle sur leurs autorisations.

Cependant, les contrôles d’accès présentés précédemment sont gérés de manière assez générique : un utilisateur a accès à telle ou telle partie de l’application. Cela ne permet pas encore de définir si l’utilisateur a accès à tel objet, par exemple.

Il existe des solutions Open Source et SaaS d’autorisation plus granulaires, par exemple Casbin ou Permit.io. Auth0 travaille actuellement sur un système de contrôle d’accès plus granulaire, en complément de son offre OIDC : Fine Grained Authorization.

Les modèles d'autorisations sont un sujet d'étude à eux seuls, qui feront peut-être l'objet d'un autre article.

Retrouvez cette architecture sur le dépôt GitHub de la démo d’API Platform.

Aller plus loin #

Créer un domaine OIDC depuis une configuration JSON

Keycloak fournit un service permettant de charger une configuration JSON pour créer un domaine contenant des clients, des utilisateurs, etc. : Keycloak Config CLI.

Sa mise en place se fait très facilement avec Docker Compose :

# docker-compose.yml
version: "3.8"

services:
  # ...

  keycloak-config-cli:
    image: bitnami/keycloak-config-cli:5-debian-11
    environment:
      KEYCLOAK_URL: http://caddy/oidc/
      KEYCLOAK_USER: ${KEYCLOAK_USER:-admin}
      KEYCLOAK_PASSWORD: ${KEYCLOAK_PASSWORD:-!ChangeMe!}
      KEYCLOAK_AVAILABILITYCHECK_ENABLED: true
      KEYCLOAK_AVAILABILITYCHECK_TIMEOUT: 120s
      IMPORT_FILES_LOCATIONS: '/config/*'
    depends_on:
      - keycloak
    volumes:
      - ./keycloak-config:/config
// keycloak-config/realm-demo.json
{
  "realm": "demo",
  "displayName": "API Platform - Demo",
  "enabled": true,
  "registrationAllowed": false,
  "users": [
    {
      "username": "admin",
      "enabled": true,
      "emailVerified": true,
      "firstName": "Chuck",
      "lastName": "NORRIS",
      "email": "admin@example.com",
      "credentials": [
        {
          "type": "password",
          "value": "Pa55w0rd"
        }
      ]
    }
  ],
  "clients": [
    {
      "clientId": "api-platform-swagger",
      "enabled": true,
      "redirectUris": ["*"],
      "webOrigins": ["*"],
      "publicClient": true
    },
    {
      "clientId": "api-platform-pwa",
      "enabled": true,
      "redirectUris": ["*"],
      "webOrigins": ["*"],
      "publicClient": true
    }
  ]
}
React-Admin et Keycloak

Votre PWA est une application React-Admin ? Jetez un œil au plugin ra-keycloak.

Attention cependant : si votre PWA intègre à la fois un front et une partie React-Admin (comme c’est le cas de la démo d’API Platform), il n’est pas possible de combiner ra-keycloak avec NextAuth.js. Auquel cas, il est préférable d’utiliser uniquement NextAuth.js.

Next.js Middleware + NextAuth.js avec un seul fournisseur

Par défaut, NextAuth.js propose une page intermédiaire laissant le choix à l’utilisateur du serveur d’autorisation. Mais si la PWA n’en appelle qu’un seul (par exemple, le serveur d’autorisation de l’entreprise), cette page peut s’avérer inutile. Pour éviter cette page intermédiaire, il est recommandé de nommer son provider lors de l’implémentation de NextAuth.js (par exemple : “id: ‘keycloak’”) et préciser ce nom à l’appel de la méthode signIn : “signIn(‘keycloak’)”.

Si l’on souhaite sécuriser tout ou partie d’une PWA, la combinaison Next.js Middleware + NextAuth.js semble plus adaptée. Cependant, cette approche ne permet pas de préciser un provider en particulier. Lors de la connexion, l’utilisateur tombe alors sur la page intermédiaire.

Pourquoi configurer un seul client OAuth pour la PWA ?

Dans le cadre de la démo d’API Platform, la PWA contient à la fois un front public et une administration privée. L’administration ne représente qu’une partie de la PWA. C’est la PWA qui est le client OAuth, un seul client OAuth suffit.

Seule l’administration est privée, il est alors simple de sécuriser cette PWA grâce à NextAuth.js.

Cependant, si le front et l’administration étaient des applications distinctes, il faudrait alors créer deux clients OAuth distincts.

Quel algorithme choisir pour la signature du jeton ?

Algorithmes symétriques vs asymétriques, taille de clé, algorithmes dépréciés pour raisons de sécurité... Comment choisir le bon algorithme de signature du jeton ?

Cet article présente plusieurs informations utiles à ces questions : https://www.linkedin.com/advice/0/how-do-you-choose-right-jwt-signing-algorithm.

Vincent Chalamon

Vincent Chalamon

Lead developer & consultant & API Platform core-team

Mots-clésAPI Platform, Sécurité

Le blog

Pour aller plus loin