Jibé Barth

đŸ‡«đŸ‡· Web developer

đŸ‡«đŸ‡· API Platform, Oauth, & Swagger UI

Published Jan 11, 2020

Api Platform, OAuth & Swagger

Aujourd'hui, nous allons voir comment utiliser une authentification OAuth, pour notre projet API Platform, Ă  travers Swagger UI.

Cela nous permettra d'activer plusieurs services OAuth, tels que Google, Github, ou encore Facebook.

Je pars du principe que si vous arrivez la, vous connaissez un minimum l'utilisation de Symfony et de API-Platform. Je vais donc passer trĂšs vite sur l'initialisation.

TOC

Création d'un projet

Pour notre projet, nous allons rapidement créer une application symfony avec API-Platform

composer create-project symfony/website-skeleton api-oauth
cd api-oauth
composer req api
symfony local:server:start -d

J'utilise le website-skeleton pour avoir tous les composants Symfony, et ne pas a avoir a faire de multiples composer require. L'utilisateur aguerri partira bien Ă©videmment du skeleton tout cours.

Initialisation de la plateforme

Premierement, nous allons altérer la configuration d'api platform, pour repasser sur swagger v2 plutot que la v3.

# config/packages/api_platform.yml
api_platform:
    #...
    swagger:
        versions: [2]

Cette Ă©tape pourra ĂȘtre Ă©vitĂ© quand cette PR sera mergĂ©e et released.

Puis, créons une entité de test :

php bin/console make:entity Book --api-resource

Et une entitée user (sans besoin de hash password )

php bin/console make:user

Et on met Ă  jour la BDD.

php bin/console d:s:u --force

Création d'un endpoint de test

Nous allons créé un endpoint de test qui se contentera de retourner l'utilisateur.

php bin/console make:controller WhoAmIController --no-template

Alterons ce controleur pour le rendre invokable, et pour retourner l'utilisateur courant :

// src/Controller/WhoAmIController.php
<?php
declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\User\UserInterface;

final class WhoAmIController extends AbstractController
{
    public function __invoke(): ?UserInterface
    {
        return $this->getUser();
    }
}

Allons modifier la classe User pour la définir en tant que ressource ApiPlatform, et ajoutons dedans notre controller.

// src/Entity/User.php
<?php
declare(strict_types=1);
    
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use App\Controller\WhoAmIController;

/**
 * @ApiResource(
 *     collectionOperations={
 *          "get", "post",
 *          "whoami"= {
 *              "method"="GET",
 *              "path"="/whoami",
 *              "controller"=WhoAmIController::class,
 *          },
 *     }
 * )
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User implements UserInterface 
{
    // ...
}

Je rajoute le controleur dans les collectionOperations , mais il conviendrai mieux dans itemOperations. Je ne souhaites juste pas m'embĂȘter a devoir enlever le champ id du swagger pour l'instant.

Si on exécute le endpoint /api/whoami, on reçoit null. Parfait. Passons à l'OAuth.

Récupérer des crédentials Oauth pour se connecter avec Google

Notre premier authenticator sera Google. Depuis la console developper, créeons d'abord un "Ecran d'autorisation Oauth". (Il n'est pas nécessaire de renseigner les domaines pour l'instant).

Puis, toujours dans cette console, créeons des identifiants "ID Client Oauth".

Ici, il est important de rajouter une Url de redirection autorisée ainsi en adaptant votre domaine:

https://127.0.0.1:8000/bundles/apiplatform/swagger-ui/oauth2-redirect.html

Puis on stock notre ID et SECRET dans le fichier .env

#.env
# ...
OAUTH_GOOGLE_ID="monid"
OAUTH_GOOGLE_SECRET="monsecret"

Completons la configuration d'API Platform :

# config/packages/api_platform.yaml
api_platform:
    # ...
    oauth:
        enabled: true
        clientId: '%env(OAUTH_GOOGLE_ID)%'
        clientSecret: '%env(OAUTH_GOOGLE_SECRET)%'
        type: 'oauth2'
        # The oauth flow grant type.
        flow: 'authorizationCode'
        # The oauth authentication url.
        authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth'
        # The oauth token url.
        tokenUrl: 'https://www.googleapis.com/oauth2/v4/token'
        # The oauth scopes.
        scopes:
            email: 'Allow to retrieve user email'

Nous pouvons maintenant tester l'authorization depuis notre UI Swagger.

Une barre apparait avec un bouton "Authorize" dans la documentation :

image

Lorsqu'on clique dessus, une popin apparait :

image

On reclique sur Authorize, un nouvel onglet s'ouvre sur Google, puis nous somme redirigé sur la documentation. L'OAuth s'est bien passé.

Authentication

Nous sommes a présent bien connecté via l'Oauth de google. Pour chaque appel que nous faisons a l'API, un header Authorization: Bearer accessToken est ajouté.

Mais si nous appelons le endpoint /api/whoami, il retourne toujours null. Voyons comment récupérer notre utilsateur.

Tout d'abord, générons un GuardAuthenticator :

php bin/console make:auth

Lorsque cette commande le demande, choisissez "Empty Authenticator"

Puis récupérons league/oauth2-google

composer require league/oauth2-google

Dans notre services.yaml, créeons un service pour le google Provider

services:
    # ...
    League\OAuth2\Client\Provider\Google:
        class: League\OAuth2\Client\Provider\Google
        arguments:
            - {clientId: '%env(OAUTH_GOOGLE_ID)%', clientSecret: '%env(OAUTH_GOOGLE_SECRET)%'}

Et injectons ce service dans notre authenticator :

// src/Security/GoogleOauthAuthenticator.php
<?php
declare(strict_types=1);

namespace App\Security;

use League\OAuth2\Client\Provider\Google;
use Doctrine\ORM\EntityManagerInterface;
//...
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;

final class GoogleOauthAuthenticator extends AbstractGuardAuthenticator
{
    /**
     * @var \League\OAuth2\Client\Provider\Google
     */
    private $client;
    /**
     * @var \Doctrine\ORM\EntityManagerInterface
     */
    private $entityManager;

    public function __construct(Google $client, EntityManagerInterface $entityManager)
    {
        $this->client = $client;
        $this->entityManager = $entityManager;
    }

Completons maintenant les différentes méthodes de notre GoogleOauthAuthenticator.

La méthode support permet de vérifier si les conditions sont bien remplies pour qu'on appelle la classe. Dans notre cas, il s'agit de vérifier la présence du nouvel header authorization.

    public function supports(Request $request): bool
    {
        return $request->headers->has('authorization');
    }

La méthode getCredentials permet d'extraire les crédentials nécessaire pour la récupération de notre Utilisateur.

    public function getCredentials(Request $request)
    {
        $accessToken = explode(' ', $request->headers->get('authorization'))[1];

        return new \League\OAuth2\Client\Token\AccessToken(['access_token' => $accessToken]);
    }

Ici, on explose le contenu du header, le token est la deuxiĂšme partie.

La méthode getUser doit retrouver l'utilisateur à partir des crédentials. C'est ici qu'on va utiliser notre nouveau service.

    public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
    {
        /** @var \League\OAuth2\Client\Provider\GoogleUser $googleUser */
        $googleUser = $this->client->getResourceOwner($credentials);

        $email = $googleUser->getEmail();
		
        try {
            $user = $userProvider->loadUserByUsername($email);
        } catch (UsernameNotFoundException $exception) {
            $user = (new User())->setEmail($email);
            $this->entityManager->persist($user);
            $this->entityManager->flush();
        }

        return $user;
    }

Enfin, la méthode checkCredential doit toujours retourner true, et la methode supportsRememberMe retournera false.

    public function checkCredentials($credentials, UserInterface $user): bool
    {
        return true;
    }
    public function supportsRememberMe(): bool
    {
        return false;
    }

Et c'est tout. Les autres méthodes peuvent rester vide pour l'instant, il conviendra de le remplir correctement le temps venu.

Si dorĂ©navant, nous appelons l'endpoint /api/whoami, notre utilisateur va ĂȘtre renvoyĂ©.

Un autre OAuth provider : Github

La plupart du temps, nous proposons Ă  nos utilisateurs plusieurs moyen de se connecter. Voyons comment ajouter une deuxieme authentification avec Github OAuth.

Commençons par créer une application OAuth sur Github et ajoutons nos nouveaux credentials dans le fichier .env.

#.env
#...
OAUTH_GITHUB_ID=monid
OAUTH_GITHUB_SECRET=monsecret

Remplaçons temporairement la configuration d'API Platform pour utiliser Github en OAuth.

#config/packages/api_platform.yaml
api_platform:
    # ...
    oauth:
        enabled: true
        clientId: '%env(OAUTH_GITHUB_ID)%'
        clientSecret: '%env(OAUTH_GITHUB_ID)%'
        type: 'oauth2'
        # The oauth flow grant type.
        flow: 'authorizationCode'
        # The oauth authentication url.
        authorizationUrl: 'https://github.com/login/oauth/authorize'
        # The oauth token url.
        tokenUrl: 'https://github.com/login/oauth/access_token'
        # The oauth scopes.
        scopes:
            'user:email': 'Allow to retrieve user email'

Si nous réessayons le flow OAuth depuis Swagger UI, une erreur survient :

Auth Error TypeError: Failed to fetch

En inspectant les requĂȘtes depuis l'inspecteur web, on se rend compte que l'url pour rĂ©cupĂ©rer l'accessToken ne prend pas en charge les CORS.

Nous devons donc créer un controlleur qui s'occupera de récupérer notre AccessToken.

php bin/console make:controller GithubTokenController --no-template

Et nous allons utiliser le paquet league/oauth2-github qui fera l'appel pour nous :

composer require league/oauth2-github

De la mĂȘme maniĂšre que pour Google, ajoutons ce provider en tant que service :

#config/services.yaml

services:
    # ...
    League\OAuth2\Client\Provider\Github:
        class: League\OAuth2\Client\Provider\Github
        arguments:
            - {clientId: '%env(OAUTH_GITHUB_ID)%', clientSecret: '%env(OAUTH_GITHUB_SECRET)%'}

Injectons ce service dans notre controller et renvoyons l'accessToken :

// src/Controller/GithubTokenController.php
<?php
declare(strict_types=1);

namespace App\Controller;

use League\OAuth2\Client\Provider\Github;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

final class GithubTokenController
{
    /**
     * @Route("/github/token", name="github_token")
     */
    public function index(Request $request, Github $client): JsonResponse
    {
        return new JsonResponse(
            $client->getAccessToken('authorization_code', [
                'code' => $request->request->get('code'),
            ])
        );
    }
}

Et on change la valeur de tokenUrl dans la config d'API Platform par notre nouvelle route :

#config/packages/api_platform.yaml
api_platform:
    # ...
    oauth: 
        # ...
        tokenUrl: '/github/token'

Si nous réessayons le flow d'authorization sur Swagger UI, tout devrait bien passer maintenant.

Pour gérer l'authentification, nous allons devoir, comme auparavant pour Google, créer un GuardAuthenticator.

php bin/console make:auth

A la création, un avertissement vous préviendra du comportement lorsque nous avons plusieurs Authenticators et vous demandera de choisir l'entry_point par défaut. Choisissez celui nouvellement créé.

Plus d'info sur la documentation.

Le principe est le mĂȘme que pour celui de Google, il faudra juste injecter le GithubProvider Ă  la place.

Si on essaye d'appeler l'endpoint /api/whoami, nous obtenons ... une erreur.

{
	"type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
	"title": "An error occurred",
	"detail": "invalid_request"
}

En utilisant le profiler, on peut voir que l'authentification s'est bien passé, nous avons un utilisateur loggé, mais on passe ensuite dans le GoogleOAuthAuthenticator, qui essaye de récupérer les information avec un accessToken qui vient de Github, et on a donc une exception.

Pour palier à ça, nous pouvons faire un try/catch dans les GuardAuthenticators au moment de récuperer les utilisateurs, et retourner null si l'appel lÚve une erreur.

// src/Security/GoogleOAuthAuthenticator.php
// ...
    public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
    {
        try {
            /** @var \League\OAuth2\Client\Provider\GoogleUser $googleUser */
            $googleUser = $this->client->getResourceOwner($credentials);
        } catch (\Throwable $throwable) {
            return null;
        }

        //...
        return $user;
    }

Mais nous pouvons aussi regrouper les deux Guard en un seul. Pour cela, dans le services.yaml, ajoutons un tag sur nos OAuth providers :

#config/services.yaml
services:
    # ...
    League\OAuth2\Client\Provider\Google:
        class: League\OAuth2\Client\Provider\Google
        arguments:
            - {clientId: '%env(OAUTH_GOOGLE_ID)%', clientSecret: '%env(OAUTH_GOOGLE_SECRET)%'}
        tags:
            - {name: 'oauth_provider'}

    League\OAuth2\Client\Provider\Github:
        class: League\OAuth2\Client\Provider\Github
        arguments:
            - {clientId: '%env(OAUTH_GITHUB_ID)%', clientSecret: '%env(OAUTH_GITHUB_SECRET)%'}
        tags:
            - {name: 'oauth_provider'}

Retirons les guard actuels dans notre security.yaml

#config/packages/security.yaml
security:
    # ...
    firewalls:
        #...
        main:
            anonymous: lazy
-            guard:
-                authenticators:
-                    - App\Security\GithubOAuthAuthenticator
-                    - App\Security\GoogleOauthAuthenticator
-                entry_point: App\Security\GithubOAuthAuthenticator

Et créons un nouveau Guard : ChainOAuthAuthenticator.

// src/Security/ChainOAuthAuthenticator
<?php
declare(strict_types=1);

namespace App\Security;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use League\OAuth2\Client\Token\AccessToken;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;

final class ChainOAuthAuthenticator extends AbstractGuardAuthenticator
{
    /**
     * @var iterable<\League\OAuth2\Client\Provider\AbstractProvider>
     */
    private $oauthProviders;
    /**
     * @var \Doctrine\ORM\EntityManagerInterface
     */
    private $entityManager;

    public function __construct(iterable $oauthProviders, EntityManagerInterface $entityManager)
    {
        $this->oauthProviders = $oauthProviders;
        $this->entityManager = $entityManager;
    }

    public function supports(Request $request): bool
    {
        return $request->headers->has('authorization');
    }

    public function getCredentials(Request $request): AccessToken
    {
        $accessToken = explode(' ',$request->headers->get('authorization'))[1];

        return new AccessToken(['access_token' => $accessToken]);
    }

    public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
    {
        $oauthUser = null;
        /** @var \League\OAuth2\Client\Provider\AbstractProvider $oauthProvider */
        foreach ($this->oauthProviders as $oauthProvider) {
            try {
                $oauthUser = $oauthProvider->getResourceOwner($credentials);
                break;
            } catch (\Throwable $throwable) {
                // This provider doesn't support current accessToken
            }
        }

        if (null === $oauthUser) {
            // No OAuth user found
            return null;
        }

        $email = $oauthUser->getEmail();

        try {
            $user = $userProvider->loadUserByUsername($email);
        } catch (UsernameNotFoundException $exception) {
            $user = (new User())->setEmail($email);
            $this->entityManager->persist($user);
            $this->entityManager->flush();
            $this->entityManager->refresh($user);
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user): bool
    {
        return true;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new JsonResponse($message, JsonResponse::HTTP_FORBIDDEN);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // No need to redirect, as it's a stateless call.
        // Let the request continue to be handled by the controller
        return null;
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
        $message = 'Auth header required';
        if (null !== $authException) {
            $message = strtr($authException->getMessageKey(), $authException->getMessageData());
        }

        return new JsonResponse($message, JsonResponse::HTTP_UNAUTHORIZED);
    }

    public function supportsRememberMe(): bool
    {
        return false;
    }
}

Puis modifions le services.yaml pour injecter automatiquement nos providers taggés.

#config/services.yaml

services:
    _defaults:
        autowire: true
        autoconfigure: true
        bind:
            iterable $oauthProviders: !tagged_iterator oauth_provider
    # ...

Notre ChainOAuthAuthenticator s'occupera donc maintenant de tester notre AccessToken avec les différents OAuthProviders.

Plusieurs OAuth Login sur Swagger UI

Comme vu précedemment, API Platform ne propose pas (encore) la possibilité de rajouter plusieurs OAuth dans sa configuration.

Swagger UI dans sa v2 le supporte, tout comme la v3. Cependant, dans la v2, les champs client_id et client_secret ne s'affichent pas, et nous devons pouvoir les différenciers.

De mĂȘme, Swagger UI propose une fonction pour initialiser ces champs, mais est limitĂ© Ă  un seul OAuth login. Donc si nous en affichons plusieurs, ils utiliseront tous les mĂȘmes valeurs.

Pour afficher nos OAuth login sur le Swagger UI d'API Platform, nous allons donc "désactiver" le support OAuth d'API Platform, et au passage activer la v3 :

# config/packages/api_platform.yaml
api_platform:
    # ...
    swagger:
        versions: [3]
    oauth:
        enabled: false

DĂ©finissons nos flows OAuth dans nos parametres

#config/services.yaml
parameters:
    oauths:
        github:
            type: oauth2
            description: OAuth authorization code Grant
            flows:
                authorizationCode:
                    tokenUrl: '/github/token'
                    authorizationUrl: 'https://github.com/login/oauth/authorize'
                    scopes:
                        'user:email': 'Allow to retrieve user email'
        google:
            type: oauth2
            description: OAuth authorization code Grant
            flows:
                authorizationCode:
                    tokenUrl: 'https://www.googleapis.com/oauth2/v4/token'
                    authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth'
                    scopes:
                        'email': 'Allow to retrieve user email'
services:
    # ...

Et créeons un SwaggerDecorator, auquel on passe notre définition de flows OAuth

# config/services.yaml
#...
services:
    # ...
    App\Swagger\OAuthSwaggerDecorator:
        decorates: 'api_platform.swagger.normalizer.api_gateway'
        arguments: [ '@App\Swagger\OAuthSwaggerDecorator.inner', '%oauths%' ]
        autoconfigure: false
// src/Swagger/OAuthSwaggerDecorator.php
<?php
declare(strict_types=1);

namespace App\Swagger;

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class OAuthSwaggerDecorator implements NormalizerInterface
{
    private $decorated;
    /**
     * @var array
     */
    private $oauths;

    public function __construct(NormalizerInterface $decorated, array $oauths)
    {
        $this->decorated = $decorated;
        $this->oauths = $oauths;
    }

    public function normalize($object, string $format = null, array $context = [])
    {
        // Retrieve swagger docs
        $docs = $this->decorated->normalize($object, $format, $context);

        // Add our flows in securitySchemes
        $docs['components']['securitySchemes'] = $this->oauths;
        $docs['security'] = [array_map(static function($value): array {
            return [];
        }, $this->oauths)];
        
        return $docs;
    }

    public function supportsNormalization($data, string $format = null)
    {
        return $this->decorated->supportsNormalization($data, $format);
    }
}

Swagger UI affiche maintenant les deux mode de connection OAuth dans sa popin.

Malheureusement, les champs clientId et clientSecret sont vide. Pour pouvoir appeler nos service, on devra les renseigner.

Comme dis prĂ©cedemment, Swagger UI ne supporte pas de pouvoir renseigner ces champs lorsqu'on a plusieurs authentifications. Ça arrivera peut-ĂȘtre un jour, suivez cette issue.

En attendant, nous allons tweaker l'interface pour remplir ces champs pour nous.

Pour cela, il va nous falloir faire un override du template SwaggerUi, injecter dedans nos information de client, et enfin ajouter des boutons pour changer à la volée.

Pour l'override du template, créer un fichier templates/bundle/ApiPlatformBundle/SwaggerUi/index.html.twig :

{% extends '@!ApiPlatform/SwaggerUi/index.html.twig' %}
{% block javascript %}
    {{ parent() }}
    <script >
        var oauths = {{ oauth_providers()|raw }};
    </script>
    <script src="{{ asset('swagger-oauth.js') }}"></script>
{% endblock %}

La fonction oauth_providers nous permet de rajouter la configuration de nos providers OAuth. Ajoutons cette fonction :

php bin/console make:twig-extension OAuthExtension
// src/Twig/OAuthExtension
<?php
declare(strict_types=1);

namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;

final class OAuthExtension extends AbstractExtension
{
    /**
     * @var iterable<\League\OAuth2\Client\Provider\AbstractProvider>
     */
    private $oauthProviders;

    public function __construct(iterable $oauthProviders)
    {
        $this->oauthProviders = $oauthProviders;
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction('oauth_providers', [$this, 'oauthProviders']),
        ];
    }

    public function oauthProviders(): string
    {
        $data = [];
        /** @var \League\OAuth2\Client\Provider\AbstractProvider $provider */
        foreach ($this->oauthProviders as $provider)
        {
            $reflectionClass = new \ReflectionClass($provider);
            
            $clientIdProperty = $reflectionClass->getProperty('clientId');
            $clientIdProperty->setAccessible(true);

            $clientSecretProperty = $reflectionClass->getProperty('clientSecret');
            $clientSecretProperty->setAccessible(true);

            $clientId = $clientIdProperty->getValue($provider);
            $clientSecret = $clientSecretProperty->getValue($provider);

            $data[] = [
                'name' => strtolower($reflectionClass->getShortName()),
                'client_id' => $clientId,
                'client_secret' => $clientSecret
            ];
        }

        return json_encode($data);
    }
}

Ici, on réutilise l'injection des providers taggé oauth_provider, afin de faire évoluer directement le rendu quand on ajoutera de nouveaux provider.

Maintenant, rajoutons le javascript qui va créer les boutons et appeler la fonction initOauth de swagger :

// public/swagger-oauth.js
'use strict';

window.addEventListener('load', function() {
    // Same in init-swagger-ui provided by API Platform
    const data = JSON.parse(document.getElementById('swagger-data').innerText);
    const ui = SwaggerUIBundle({
        spec: data.spec,
        dom_id: '#swagger-ui',
        validatorUrl: null,
        oauth2RedirectUrl: data.oauth.redirectUrl,
        presets: [
            SwaggerUIBundle.presets.apis,
            SwaggerUIStandalonePreset,
        ],
        plugins: [
            SwaggerUIBundle.plugins.DownloadUrl,
        ],
        layout: 'StandaloneLayout',
    });

    const authWrapper = document.getElementsByClassName('auth-wrapper')[0];

    // Create an info about current credentials
    const info = document.createElement("p");
    let oauthTxt = "Current OAuth Credentials : ";
    info.innerText = oauthTxt + "None";

    authWrapper.parentNode.insertBefore(info, authWrapper);

    oauths.forEach(function (elem) {
        const btn = document.createElement("button");
        btn.classList = 'btn';
        btn.innerText = 'OAuth ' + elem.name;
        btn.onclick = function() {
            ui.initOAuth({
                clientId: elem.client_id,
                clientSecret: elem.client_secret
            });
            info.innerText = oauthTxt + elem.name;
        };
        // Add btn near to authorize button
        authWrapper.appendChild(btn);
    });
});

Voici le rendu :

image

Au clic sur le bouton OAuth google, les credentials de Google seront préremplis dans la popin.