Backstage Plugins by Example: Part 3
Here we explore securing our backend Plugin API.
This is part of a series of articles starting with Backstage Plugins by Example: Part 1.
In the previous article, we created a backend Plugin that exposed an endpoint, api/my-plugin/health and then accessed from the frontend Plugin. Thinking about that implementation, we observe that the endpoint was not secured, i.e., we could access it without any authentication / authorization. Here we explore securing it.
Updating the Frontend Plugin
The first step to securing the endpoint is to have the frontend Plugin pass an authentication (identity / ID) token when accessing it.
As a plugin developer, there are two main touchpoints for identities: the IdentityApi exported by @backstage/core-plugin-api via the identityApiRef, and a not yet existing middleware exported by @backstage/backend-common.
The IdentityApi gives access to the signed-in user’s identity in the frontend. It provides access to the user’s ID, lightweight profile information, and an ID token used to make authenticated calls within Backstage.
— Using authentication and identity
As this does not give sufficient information on how to proceed, we can turn to looking for patterns in the kubernetes core feature for the solution.
Here we update our API client to pass an Authorization header with the request by using an IdentityApi object.
plugins/my-plugin/src/api/MyPluginBackendClient.ts
We now need to update our Plugin configuration to pass an IdentityApi object to the API client.
plugins/my-plugin/src/plugin.ts
After re-building / starting the Backstage App image, we indeed see that our frontend Plugin is indeed passing an Authorization header when accessing our backend Plugin API endpoint.
Securing the Backend APIs
Unfortunately there is less information about how to use the authentication (identity / ID) token provided by the frontend Plugin in the backend Plugin.
The middleware that will be provided by @backstage/backend-common allows verification of Backstage ID tokens, and optionally loading additional information about the user. The progress is tracked in https://github.com/backstage/backstage/issues/1435.
— Using authentication and identity
Even more confusing is the following statement.
NOTE: Identity management and the SignInPage in Backstage is NOT a method for blocking access for unauthorized users, that either requires additional backend implementation or a separate service like Google’s Identity-Aware Proxy. The identity system only serves to provide a personalized experience and access to a Backstage Identity Token, which can be passed to backend plugins.
— Using authentication and identity
A quick check of one of the Core feature APIs using curl without an Authorization header actually returns data; so this essentially this means that none of the APIs are actually secured.
$ curl http://localhost:7007/api/search/query\?term\=
{"results"...}
So, if we want to secure the Backstage App, we will have to use the advice of using a third-party proxy service, e.g., Google’s Identity-Aware Proxy, to secure the APIs.
Updating the Backend Plugin
Before learning that none of the Backstage App APIs are secured, I spent some time figuring out how to secure the backend Plugin API endpoint; api/my-plugin/health.
The Backstage feature suggestion, Make backend-common provide middleware for verifying Backstage ID Tokens, provides some hints as to how to proceed; specifically that the authentication (identity / ID) token is a JSON Web Token (JWT) signed using a public key provided by a JSON Web Key Set (JWKS).
Using information from the feature suggestion and a bit of trial and error, we indeed see that the JWKS is available from the /api/auth/.well-known/jwks.json endpoint.
curl http://localhost:7007/api/auth/.well-known/jwks.json
{"keys":[{"crv":"P-256","x":"7uJOMoY9ChHfO6gnZNCJs8ABR2nFbizTNJG9mtOaJTA","y":"v4omahvGiYL_bTNEkF8m9I7aLqNXJQJLl_t0FAbBcH8","kty":"EC","kid":"947202e4-53a8-4289-9674-e9dd26d439f8","alg":"ES256","use":"sig"}]}
There are two Node.js libraries that will form the heart of our solution:
- express-jwt: This module provides Express middleware for validating JWTs (JSON Web Tokens) through the jsonwebtoken module. The decoded JWT payload is available on the request object
- jwks-rsa: A library to retrieve signing keys from a JWKS (JSON Web Key Set) endpoint
In addition to knowing the JWKS endpoint, we can determine the remaining parameters to use these libraries by copying the JWT from the Authorization header and pasting it in to the JWT debugger available at jwt.io.
In particular we will use alg (algorithm), iss (issuer), and aud (audience); it is sub that we will want to extract from the token as the user’s identity.
In addition to installing the libraries and using them to secure our backend Plugin health API endpoint, there is a bit of complexity around determining the Backstage App base URL from the value of the backend.baseUrl in app-config.yaml.
plugins/my-plugin-backend/src/service/router.ts
Because the createRouter function now requires a Config object, we have to update a number of files.
plugins/my-plugin-backend/src/service/router.test.ts
plugins/my-plugin-backend/src/service/standaloneServer.ts
packages/backend/src/plugin/my-plugin.ts
packages/backend/src/index.ts
Please note: One interesting thing here is that we can no longer run our backend Plugin by simply running yarn start
. This is because the JWKS endpoint is not available until we run the Backstage App.
After re-building / starting the Backstage App image, we indeed see that our frontend and backend Plugins now know our identity.
Wrap Up
Having exhausted myself with securing the backend Plugin API endpoint, I decided to stop writing more for this series. But there are couple of things that we now know we would use if we were to proceed.
First. we would add an annotation on the Backstage Component that we have been using to point to the AWS bucket.
Much like the frontend Plugin code where we accessed the Component metadata.name, we would access annotation with the AWS bucket name.
Following the pattern of other Plugins, we would store the credentials for accessing the AWS buckets in app-config.yaml (actually stored in reference environment variables).
Much like the backend Plugin code where we accessed the backend.baseUrl value from app-config.yaml, we would access these credentials.
The rest of the code would be not so much about Backstage, but about reading and displaying information about AWS buckets (not really the point of this article).
Whew!