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
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:
Alternatively, you may retrieve an Organization's orgid
using the https://api.snyk.io/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 deprecated 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 AppredirectUris
: The accepted callback location(s) during end-user authenticationscopes
: 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.
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:
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 theredirect_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:
Send API requests and process responses.
Keep track of data, like token expiry.
Encrypt and decrypt secret data.
Turn authorization codes into authorization tokens.
Refresh those authorization tokens.
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:
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.
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.
With the database initialization handled, we'll create some new helper methods to make reading, writing, and updating database entries simpler. Because this is a TypeScript project, we'll be creating interfaces or types around the data structures we'll be storing, so we'll create two files: ./src/interfaces/DB.ts
and ./src/util/DB.ts
:
Populate the interface file with an interface describing each piece of the authorization data we'll be storing, and a wrapping interface we can apply to the entire database:
In this tutorial, we'll need to perform three basic interactions with our database: read, write, and update.
In the file we created in ./src/util
, create a function for each. Our read function will return a Promise with the database contents; the write function will take an object that matches the AuthData
interface we just described; and the update function will attempt to rewrite an entry, returning a boolean denoting success or failure.
Prepare for API calls
Earlier, we installed the popular axios
package to handle API calls. We know that we'll need to make some repetitive calls to the same API, so we abstract some helper functions to make our code easily re-usable across the project. Create an APIHelpers.ts
file in the util
directory.
Before we fill that out, take note that while we are consistently hitting Snyk's API, we'll likely need to make requests against multiple versions of the API, depending on the endpoint's status in the migration from Snyk API v1 to Snyk REST API. One way we can handle this is by defining a TypeScript Enum and within our functions, swapping any necessary query parameters by comparing an argument to the enum's possible values.
Add the following content to a new file, or to APIHelpers.ts
if you prefer; just make sure to export it for later use.
We start by adding a single function to simplify our Apps' calls to the Snyk API. The function takes a tokenType
(either bearer or token), the token
itself, and an APIVersion
(conveniently corresponding to the enum we just defined).
Because this function is an AxiosInstance
, we can easily talk to the API's different endpoints by calling .get()
, .post()
, or any other methods usually available to such an object.
We will see it in action by defining a second async function to retrieve our Snyk Apps' Organization ID:
Make encrypt / decrypt easy
It's a good idea to encrypt the data we'll be pulling out of the API. We will define a small class for doing so. The class has two members:
secret
: The key used to encrypt datacryptr
: Instance of the Cryptr library
Configure Passport.js and the Snyk-OAuth2 strategy
We've laid the groundwork; now it's time to start doing things.
As discussed in a previous section, our app needs to send would-be authorizers to a specific token URL. We'll add an /auth
route in our Snyk App and add some authentication middleware to Express. For this, we'll use the excellent passportjs, the passport-oauth2 authentication strategy, along with Snyk's @snyk/passport-snyk-oauth2. passport
and its friends handle a large portion of what would otherwise be a lengthy and complicated authentication process.
Because passport
takes its encapsulation philosophy seriously, we'll need to handle everything else about the auth process. We need to set up an instance of the passport strategy we'll be using. We'll put our database helpers from earlier to use here as well, adding an entry into our database when we receive successful authorization.
It's worth taking the time to go over this file and make sure you've understood everything it's doing.
Update Express middleware
With our passport strategy implemented, modify app.ts
to set up the passport
middleware as shown in the next code block. Rather than calling it directly, we'll create a function called initGlobalMiddlewares()
allowing us to set up a few other middlewares at the same time:
express.json()
: Express middleware for handling JSON requestsexpress.urlencoded()
: Express middleware to handle URL-encoded callsexpressSession
: Theexpress-session
middleware package which is extended bypassport
setupPassport
: To initializepassport
setup
Handle the authorization and callback routes
The authorization and callback controllers are comparatively simple. Create two new controller files:
The AuthController
handles authentication of the App via the previously described authorization flow. This is the third step of passport
setup. Every controller class implements the controller interface which has two members: the path and the router.
This controller handles the /auth
route, which is what we'll use to send users (via passport
) to the Snyk website for authorization approval.
Once a user approves authorization to our Snyk App via the Snyk website, the user is kicked back to our callback URI, /callback
. We'll handle this route similarly, invoking passport again. This is the final step of user authorization.
The CallbackController
accepts requests on /callback
, but also creates two sub-routes, /callback/success
and /callback/failure
, to handle the different possible outcomes it might receive from Snyk.
Before we're done, we need to make sure we add a reference to our new controllers in our index.ts
.
Refresh token management
If we build and run our Snyk App at this point, hitting the /auth
route will successfully jump us out to the Snyk authorization portal and, provided we confirm the authorization, we'll get kicked back to our local app's callback route at /callback
. If we had a very simplistic, one-off use case, we could end things here. But there's one more piece of the puzzle we should figure out if we're going to keep our user's authorization fresh; that is token expiry.
If you ran the app to test things, take a look at database entries. If you've been following along, you should see something like this:
That expires_in
value will continue to count down until 0. If it does, the user will need to re-authorize.
To keep our access token from going stale, we need to make a POST request using our refresh_token
to get an updated access_token
. See Set up the refresh token exchange.
We can automate this process in our Snyk App by utilizing Axios interceptors to intercept the requests we make and ensure we have an up-to-date access_token
.
Create the file ./src/util/interceptors.ts
, importing all the packages, classes, and so on that we'll need at the top:
We'll add a total of three interceptors.
The first, refreshTokenReqInterceptor
will refresh the auth_token
using the refresh_token
when the auth_token
expires. It takes an AxiosRequestConfig request as an argument that can be used in the interceptor for additional checks.
refreshTokenRespInterceptor
will be used during request responses . Refresh/retry the token only when the response being received is a 401 Unauthorized; this is what Passport returns when things go awry.
Lastly, refreshAndUpdateDb
refreshes the access token for a given database record and updates the database again before returning the newly refreshed token.
With our interceptors defined, the only thing we need to do is update our callSnykApi
function to utilize them. Interceptors are methods of the axiosInstance
object, so we'll add them after the axios.create()
call and before the function's return
.
Wrap-up
If you've made it this far, congratulations! You've learned how to register a Snyk App with Snyk, configure the authorization flow, keep the auth_token
from getting stale, and set up a great starting point using TypeScript.
In the next module of this tutorial, we'll add a template system and configure our app to show users all of their projects from Snyk in our App.
Last updated