DEV Community

Cover image for Create an Authentication Provider for VS Code
Deepesh Kumar
Deepesh Kumar

Posted on

Create an Authentication Provider for VS Code

Introduction

By default, VS Code comes with built-in support for GitHub, GitHub Enterprise, and Microsoft authentication providers. If you're utilizing a different service or have your own authentication system, you'll probably need to develop your own authentication provider.

You can find comprehensive guides on how to do this in two main references:

GitHub Authentication Provider: [https://github.com/microsoft/vscode/tree/main/extensions/github-authentication]
Microsoft Authentication Provider: [https://github.com/microsoft/vscode/tree/main/extensions/microsoft-authentication]

VS code auth flow

Detail

To initiate the creation of your authentication provider, begin by crafting a new class that adheres to the AuthenticationProvider interface.

For demonstration purposes, I'll illustrate the process using Auth0.

The authentication provider necessitates the implementation of the following methods:

onDidChangeSessions: This acts as an event handler for changes in authentication sessions.
getSessions: VS Code utilizes this method to determine the existence of authenticated sessions.
createSession: This method is invoked to establish a new authenticated session, akin to logging in.
removeSession: Similar to logging out of a service, this function removes a cached session.

The structure of this class is outlined as follows:

import { authentication, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationSession, Disposable, EventEmitter, ExtensionContext } from "vscode";

export const AUTH_TYPE = `auth0`;
const AUTH_NAME = `Auth0`;

export class Auth0AuthenticationProvider implements AuthenticationProvider, Disposable {
  private _sessionChangeEmitter = new EventEmitter<AuthenticationProviderAuthenticationSessionsChangeEvent>();
  private _disposable: Disposable;

  constructor(private readonly context: ExtensionContext) {
    this._disposable = Disposable.from(
      authentication.registerAuthenticationProvider(AUTH_TYPE, AUTH_NAME, this, { supportsMultipleAccounts: false })
    )
  }

  get onDidChangeSessions() {
    return this._sessionChangeEmitter.event;
  }

  /**
   * Get the existing sessions
   * @param scopes 
   * @returns 
   */
  public async getSessions(scopes?: string[]): Promise<readonly AuthenticationSession[]> {
    return [];
  }

  /**
   * Create a new auth session
   * @param scopes 
   * @returns 
   */
  public async createSession(scopes: string[]): Promise<AuthenticationSession> {
    return null as any as AuthenticationSession;
  }

  /**
   * Remove an existing session
   * @param sessionId 
   */
  public async removeSession(sessionId: string): Promise<void> {

  }

  /**
   * Dispose the registered services
   */
  public async dispose() {
    this._disposable.dispose();
  }
}
Enter fullscreen mode Exit fullscreen mode

Establishing a session

To begin, let's focus on creating the session. As previously noted, this step involves logging into your authentication service.

For Auth0, the process entails logging in, retrieving the token, fetching user information, and storing this data as the authentication session.

The createSession method appears as follows:

const AUTH_TYPE = `auth0`;
const AUTH_NAME = `Auth0`;
const SESSIONS_SECRET_KEY = `${AUTH_TYPE}.sessions`

export class Auth0AuthenticationProvider implements AuthenticationProvider, Disposable {

  // Shortened for brevity

  public async createSession(scopes: string[]): Promise<AuthenticationSession> {
    try {
      const token = await this.login(scopes);
      if (!token) {
        throw new Error(`Auth0 login failure`);
      }

      const userinfo: { name: string, email: string } = await this.getUserInfo(token);

      const session: AuthenticationSession = {
        id: uuid(),
        accessToken: token,
        account: {
          label: userinfo.name,
          id: userinfo.email
        },
        scopes: []
      };

      await this.context.secrets.store(SESSIONS_SECRET_KEY, JSON.stringify([session]))

      this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] });

      return session;
    } catch (e) {
      window.showErrorMessage(`Sign in failed: ${e}`);
      throw e;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The VS Code's secrets store stores the authentication session, which is then utilized by the getSessions and removeSession methods.

login method

The logic for the login functionality is encapsulated within the login method:

const AUTH_TYPE = `auth0`;
const AUTH_NAME = `Auth0`;
const CLIENT_ID = `3GUryQ7ldAeKEuD2obYnppsnmj58eP5u`;
const AUTH0_DOMAIN = `dev-txghew0y.us.auth0.com`;
const SESSIONS_SECRET_KEY = `${AUTH_TYPE}.sessions`

export class Auth0AuthenticationProvider implements AuthenticationProvider, Disposable {

  // Shortened for brevity

  private async login(scopes: string[] = []) {
    return await window.withProgress<string>({
      location: ProgressLocation.Notification,
      title: "Signing in to Auth0...",
      cancellable: true
    }, async (_, token) => {
      const stateId = uuid();

      this._pendingStates.push(stateId);

      if (!scopes.includes('openid')) {
        scopes.push('openid');
      }
      if (!scopes.includes('profile')) {
        scopes.push('profile');
      }
      if (!scopes.includes('email')) {
        scopes.push('email');
      }

      const scopeString = scopes.join(' ');

      const searchParams = new URLSearchParams([
        ['response_type', "token"],
        ['client_id', CLIENT_ID],
        ['redirect_uri', this.redirectUri],
        ['state', stateId],
        ['scope', scopeString],
        ['prompt', "login"]
      ]);
      const uri = Uri.parse(`https://${AUTH0_DOMAIN}/authorize?${searchParams.toString()}`);
      await env.openExternal(uri);

      let codeExchangePromise = this._codeExchangePromises.get(scopeString);
      if (!codeExchangePromise) {
        codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.handleUri(scopes));
        this._codeExchangePromises.set(scopeString, codeExchangePromise);
      }

      try {
        return await Promise.race([
          codeExchangePromise.promise,
          new Promise<string>((_, reject) => setTimeout(() => reject('Cancelled'), 60000)),
          promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise
        ]);
      } finally {
        this._pendingStates = this._pendingStates.filter(n => n !== stateId);
        codeExchangePromise?.cancel.fire();
        this._codeExchangePromises.delete(scopeString);
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

What occurs within the login method?

The login method undertakes the following tasks:

Generates a unique state ID and stores it. This state ID is subsequently verified post sign-in.
Checks if the default permission scopes are included (openid, profile, and email).
Constructs the authorize URL along with its necessary query string parameters, emphasizing the significance of the redirect_uri.
Opens the authorize URL in the browser.
Awaits the return of the token, managed within the handleUri method.

Redirecting URI for VS Code and managing the redirection

To enable your extension to receive a code/token, you must either utilize a localhost service or implement a UriHandler. The UriHandler facilitates opening your extension instance within VS Code.

The URI format is as follows:

vscode://<publisher>.<extension-name>
vscode-insider://<publisher>.<extension-name>
Enter fullscreen mode Exit fullscreen mode

The code to implement the URI Handler looks as follows:

class UriEventHandler extends EventEmitter<Uri> implements UriHandler {
  public handleUri(uri: Uri) {
    this.fire(uri);
  }
}

export class Auth0AuthenticationProvider implements AuthenticationProvider, Disposable {

  constructor(private readonly context: ExtensionContext) {
    this._disposable = Disposable.from(
      authentication.registerAuthenticationProvider(AUTH_TYPE, AUTH_NAME, this, { supportsMultipleAccounts: false }),
      window.registerUriHandler(this._uriHandler) // Register the URI handler
    )
  }

  // Shortened for brevity

   /**
   * Handle the redirect to VS Code (after sign in from Auth0)
   * @param scopes 
   * @returns 
   */
  private handleUri: (scopes: readonly string[]) => PromiseAdapter<Uri, string> = 
  (scopes) => async (uri, resolve, reject) => {
    const query = new URLSearchParams(uri.fragment);
    const access_token = query.get('access_token');
    const state = query.get('state');

    if (!access_token) {
      reject(new Error('No token'));
      return;
    }
    if (!state) {
      reject(new Error('No state'));
      return;
    }

    // Check if it is a valid auth request started by the extension
    if (!this._pendingStates.some(n => n === state)) {
      reject(new Error('State not found'));
      return;
    }

    resolve(access_token);
  }
}
Enter fullscreen mode Exit fullscreen mode

The manner in which you manage the redirection varies based on the authentication provider in use. For example, with Auth0, the token and supplementary details such as the state are furnished as URI fragments.

Conversely, if you utilize Azure AD authentication, these details are provided as query string parameters.

Upon receiving the access token in the createSession method, an authenticated session is established.

Retrieve the current sessionHyperlink icon

With session creation now established, it's time to finalize the getSessions method.

When an extension invokes authentication.getSession, VS Code triggers this method. Here, the objective is to retrieve the session data from the secret store and return the session if it exists.

const AUTH_TYPE = `auth0`;
const SESSIONS_SECRET_KEY = `${AUTH_TYPE}.sessions`

export class Auth0AuthenticationProvider implements AuthenticationProvider, Disposable {

  // Shortened for brevity


  /**
   * Get the existing sessions
   * @param scopes 
   * @returns 
   */
  public async getSessions(scopes?: string[]): Promise<readonly AuthenticationSession[]> {
    const allSessions = await this.context.secrets.get(SESSIONS_SECRET_KEY);

    if (allSessions) {
      return JSON.parse(allSessions) as AuthenticationSession[];
    }

    return [];
  }
}
Enter fullscreen mode Exit fullscreen mode

Utilizing your personalized authentication provider

Once you've implemented the authentication provider, the next step is to register it with VS Code. This can be accomplished as follows:

export async function activate(context: ExtensionContext) {

    context.subscriptions.push(
        new Auth0AuthenticationProvider(context)
    );

}
Enter fullscreen mode Exit fullscreen mode

Once it's incorporated, when you attempt to execute the authentication provider, you'll encounter this modal.

Authprovider modal example from gitpod

Top comments (0)