I have a NestJS server that uses firebase auth and a swagger ui for the playground. All of my routes request authorization, and normally I'd get a jwt token outside of ui, for example from my get-jwt.js script.

Instead of requesting the jwt token, and then manually copying that token into Swagger auth, I want to directly login with Google from the browser.

npm i @nestjs/swagger

My swagger init follows the default nestjs swagger guide:

  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('API Spec')
    .setDescription('Demo API for firebase-expo-demo app')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const documentFactory = () => SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, documentFactory);

  await app.listen(process.env.PORT ?? 3000);

Default api document, that is reachable at http://localhost:3000/api, won't be good enough. I need to extend that document with firebase google login.

That's why I had an idea to make use of the customJsStr provided by the library, and do it myself:

SwaggerModule.setup('api', app, documentFactory, {
    customJsStr: `
    (function() {
      function addScript(src, onload) {
        var s = document.createElement('script');
        s.src = src;
        s.onload = onload;
        document.head.appendChild(s);
      }

      addScript("https://www.gstatic.com/firebasejs/12.2.1/firebase-app-compat.js", function () {
        addScript("https://www.gstatic.com/firebasejs/12.2.1/firebase-auth-compat.js", initFirebaseAuth);
      });


      function initFirebaseAuth() {
        console.log('Init firebase auth')
        var firebaseConfig = {
          apiKey: "${configService.get('FB_API_KEY')}",
          authDomain: "${configService.get('FB_AUTH_DOMAIN')}",
          projectId: "${configService.get('FB_PROJECT_ID')}"
        };
        firebase.initializeApp(firebaseConfig);

        var auth = firebase.auth();
        var provider = new firebase.auth.GoogleAuthProvider();

        var bar = document.createElement('div');
        bar.innerHTML = '<button id="login">Sign in with Google</button>'
                      + '<button id="logout" style="display:none">Sign out</button>'
                      + '<span id="user" style="margin-left:8px;"></span>';
        bar.style.cssText = 'display:flex;gap:12px;align-items:center;padding:10px 16px;border-bottom:1px solid #eee;';
        document.body.insertBefore(bar, document.body.firstChild);

        var loginBtn = document.getElementById('login');
        var logoutBtn = document.getElementById('logout');
        var userSpan = document.getElementById('user');

        window.__idToken = null;

        loginBtn.onclick = function() { auth.signInWithPopup(provider); };
        logoutBtn.onclick = function() { auth.signOut(); };

        auth.onAuthStateChanged(function(user) {
          if (user) {
            user.getIdToken().then(function(t) {
              window.__idToken = t;
            });
            loginBtn.style.display = 'none';
            logoutBtn.style.display = 'inline-block';
            userSpan.textContent = 'Signed in as ' + (user.email || user.uid);
          } else {
            window.__idToken = null;
            loginBtn.style.display = 'inline-block';
            logoutBtn.style.display = 'none';
            userSpan.textContent = '';
          }
        });
      }
      function attachInterceptor() {
          if (!window.ui) return;

          const system = window.ui.getSystem();
          if (!system || !system.fn || !system.fn.fetch) return;

          const origFetch = system.fn.fetch;

          system.fn.fetch = (req) => {
            const token = window.__idToken;
            console.log('Token found', token)

            if (token) {
              if (!req.headers) req.headers = {};
              req.headers["Authorization"] = "Bearer " + token;
            }

            return origFetch(req);
          };

          console.log("Swagger interceptor attached");
        }

        function waitForUi() {
          if (window.ui && window.ui.getSystem) {
            attachInterceptor();
          } else {
            setTimeout(waitForUi, 300);
          }
        }

        waitForUi()
    })();
  `,
  });

In this script I initialize firebase auth, inject login buttons at the top of the page. When I login, I save the idToken in the global window as window.__idToken. Then I call the Swagger ui instance to modify the fetch commands to inject an Authorization header, that contains my jwt token.


You can use the extended swagger ui for any kind of server, it doesn't need to be NestJS. It's important that you need to inject html login buttons, add firebase authentication and save your idToken somewhere (localStorage, global). Last step would be to call the swagger ui request interceptor, to inject the Authorization header. My swagger ui request interceptor looks like this because it's already initialized when using nestjs swagger library.

A more elegant way would be to pass the interceptor upon initializing swagger ui:

const ui = SwaggerUIBundle({ 
  // ... rest of the config
  requestInterceptor: (req) => { 
      if (window.__idToken) {
          req.headers["Authorization"] = `Bearer ${window.__idToken}`;      
      }    
      return req; 
  },
}

Github: https://github.com/amarjanica/firebase-expo-demo

Youtube: https://youtu.be/kJtcWaFzTa8