Register the App and configure user authorization

In the previous sections of this tutorial, we set up our TypeScript project, added an Express server, and configured some basic routing. We'll be building on top of the project we created in the previous sections. It is highly recommended that you complete the previous portions of this tutorial before continuing if you have not done so already.

Creating and registering a Snyk App

We've made some good progress with our TypeScript application so far, but at the moment, that's all it is - a TypeScript application. To turn it into a bonafide Snyk App, we'll need to register our project as a new App using Snyk's API

Prerequisites

  • A Snyk account with API privileges

  • The orgid of the Snyk Organization that will be registered as the App owner

Obtaining an orgid

There are two methods for retrieving an orgid. The first is to log in to your Snyk account and visit the organization settings page of the Organization for which you wish to retrieve the ID. The path to the Organization settings page is:

https://snyk.io/org/{your-org-name}/manage/settings

Alternatively, you may retrieve an Organization's orgid using the https://snyk.io/api/v1/orgs API endpoint, using your API token in the authorization header. For details about this endpoint, view its documentation.

About Snyk Apps and the Snyk API

Snyk Apps have first-class access to the API, regardless of whether users installing the App have paid for access or not. To take advantage of this feature, Apps must use API endpoints with the domain https://api.snyk.io/rather than the conventional https://snyk.io/api/, when accessing the API within the App.

Registering our app with Snyk

Registration of a new Snyk App is a performed via a simple POST request to Snyk's API. While we could configure the App we've been building throughout this tutorial to perform the request, we'll instead make the request directly using curl to avoid creating a function that can only be run a single time.

The body of the request requires the following details:

  • name: The name of the Snyk App

  • redirectUris: The accepted callback location(s) during end-user authentication

  • scopes: The account permissions the Snyk App will ask a user to grant

A note on scopes: Once registered, Snyk Apps scopes cannot currently be changed. The only recourse is deleting the Snyk App using the Delete App API endpoint and registering the app again as a new Snyk App.

At the time of this writing, Snyk Apps is still in beta. At the moment, there is only one available scope: apps:beta. This scope allows the App to test and monitor existing projects, as well as read information about Snyk organizations, existing projects, issues, and reports.

One of the limitations of the Snyk Apps beta is that a Snyk App may only be authorized by users who have administrator access to the Organization to which the Snyk App is registered.

With your API token and orgid in hand, perform the following command in your terminal, substituting the values as necessary. For this tutorial, use http://localhost:3000/callback for the redirectUris value.

You can avoid inputting your API Token and other secrets directly into your shell by adding them as export statements in a file and sourcing the file to set them as environment variables.

curl --include \
     --request POST \
     --header "Content-Type: application/vnd.api+json" \
     --header "Authorization: token <API_TOKEN>" \
     --data-binary "{
       \"name\": \"My Awesome App\",
       \"redirectUris\": [ \"http://localhost:3000/callback\" ],
       \"scopes\": [ \"apps:beta\" ]
       }" \
     'https://api.snyk.io/rest/orgs/<ORG_ID>/apps?version='

The response from Snyk contains two important values needed to complete our Snyk App's integration: clientId and clientSecret. Store these values somewhere safe. This is the only time you will see your clientSecret from Snyk. As a warning, never share your clientSecret publicly. This is used to authenticate your App with Snyk.

Now that we've registered the app as a Snyk App, we can start adjusting our TypeScript project to allow users to authorize it.

User authorization with a Snyk App

User authentication for Snyk Apps is done by way of a webpage URL containing query parameters that match up with our Snyk App's data. We'll need to replace the query parameter values in this URL and send users to the final link in a web browser. From there they can grant account access to the Snyk App.

Once access has been provisioned, the user will be kicked back to our app's registered callbackURL, which we defined as http://localhost:3000/callback.

Essentially, our app needs to generate a link like the following and then send the user to it when it's time to authorize:

https://app.snyk.io/oauth2/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectURI}&state={state}&code_challenge={codeChallenge}&code_challenge_method=S256

Though some of the query parameters may be somewhat obvious, we will go over them. We're going to modify our Snyk App to generate this URL for our users.

  • redirect_uri: Optional values must match one of the values sent with our registration command from Registering our app with Snyk. If not passed then the first value on the Snyk App is assumed.

  • state: This is used to carry any App-specific state from this /authorize call to the callback on the redirect_uri (such as a user's ID). It must be verified in our callback to prevent CSRF attacks.

  • code_challenge: A URL-safe base64-encodes string of the SHA256 hash of a code verifier. The code verifier is a highly randomized string stored alongside the app side before calling /authorize, then sent when exchanging the returned authorization code for a token pair. This is part of Proof Key for Code Exchange (PKCE) which helps prevent authorization code interception attacks.

Once a connection is complete, the user is redirected to the provided redirect URI (our /callback route in this case) with query string parameters code and state added on, which are necessary for the next steps of authorization.

That next step involves taking the authorization code received as a query parameter in the response of the previous step and turning it into an access token. To do this, a Snyk App makes a POST request to the token endpoint: https://api.snyk.io/oauth2/token. That POST request needs some specific data in its request body, including the authorization code, client id, client secret, code_verifier and so on.

When successful, that POST request's response contains everything a Snyk App needs to communicate with Snyk on behalf of the authorizing user, namely, a refresh_token and an access token.

The access token gets used for future API calls and has a much shorter expiry than the refresh token. The refresh token can be used only one time to get a new access token when it expires. In other words, the refresh_token will no longer be usable if its own expiry time passes or if it is used to refresh the access_token. Ultimately, this means that our Snyk App will need to make frequent API calls to perform refresh_token exchanges.

Both of these tokens should be encrypted before storing them.

Updating our Snyk App to handle user authorization

Based on the preceding information, our Snyk App has some new requirements. We will need to do a few things within our TypeScript app to successfully authorize a Snyk user account with our Snyk app:

  1. Send API requests and process responses.

  2. Keep track of data, like token expiry.

  3. Encrypt and decrypt secret data.

  4. Turn authorization codes into authorization tokens.

  5. Refresh those authorization tokens.

  6. Handle errors and inform users of authorization success or failure.

From here on, we'll be doing a lot of refactoring in our Snyk App and we'll be jumping into a number of different files. To help make the process easier to follow, this tutorial uses the convention of adding a commented filepath to the first line of code snippets, describing where they belong. In your own code; these comments aren't necessary.

We'll also add a number of new packages to help address our new requirements. For convenience, go ahead and run the following in the root of your project:

npm install --save passport \
    passport-oauth2 \
    @snyk/passport-snyk-oauth2 \
    @types/passport \
    @types/uuid \
    express-session \
    axios \
    uuid \
    ejs \
    jwt-decode \
    cryptr \
    "github:dankreiger/lowdb#chore/esm-cjs-hybrid-WITH-LIB" # This allows lowdb to be used with commonjs modules
    luxon;

npm install --save-dev @types/cryptr \
    @types/ejs \
    @types/express-session \
    @types/luxon \
    @types/passport \
    @types/passport-oauth2 \
    @types/uuid

Store data and config in the Snyk App

Application configuration

Application config (for example, client secrets, api tokens, other config, and so on) should generally be stored securely and kept outside of the App itself. However, for brevity, this tutorial adds the configuration info as exportable constants in the App.ts file and leaves the actual implementation details to you. These are values that the Snyk App references in many different places.

// ./src/app.ts

...

import { v4 as uuid4 } from "uuid";

...

export const APP_BASE = "https://app.snyk.io";
export const API_BASE = "https://api.snyk.io";
export const CLIENT_ID = "[replace with your client id]";
export const CLIENT_SECRET = "[replace with your client secret]";
export const ENCRYPTION_SECRET = uuid4();
export const REDIRECT_URI = "https://localhost:3000/callback";
export const TOKEN_URL = "/oauth2/token";
export const AUTHORIZATION_URL = "/oauth2/authorize";
export const SCOPE = "apps:beta";
export const STATE = true;
...

Storing data

One of the things we'll want to do is capture some information about the users that authorize our Snyk App. Again, a true implementation is left up to you. For the purposes of this tutorial, we'll use the excellent lowdb, a small local JSON database with low overhead.

We'll first create a new middleware function in app.ts to initialize a lowdb database file at ./db/ and tell the App constructor to call it.

// ./src/app.ts

...
import * as path from "path";
import * as fs from "fs";

...

constructor(controllers: Controller[], port: number) {
  this.app = express();
  this.initDatabaseFile();
  this.initRoutes(controllers);
  this.server = this.listen(3000);
}

...

private initDatabaseFile() {
  try {