JetBrains Space Help

Verify Requests from Space

If your application is supposed to receive requests from Space (for example, a chatbot that listens to requests on some endpoint), it should be able to verify whether the incoming requests are authentic. Space provides a number of verification methods.

(Recommended) Public key

This verification method is based on asymmetric encryption. We recommend using it as the most secure.

Space SDK supports this verification type out of the box. A Space client provides the verifyWithPublicKey() method that handles the entire signature verification process. To verify the signature, the method requires the request body and the content of the X-Space-Public-Key-Signature and X-Space-Timestamp HTTP headers.

For example, this is how you can use this method to verify requests in your application:

import io.ktor.client.* import space.jetbrains.api.runtime.SpaceHttpClient import space.jetbrains.api.runtime.helpers.verifyWithPublicKey import space.jetbrains.api.runtime.withServiceAccountTokenSource import io.ktor.application.* import io.ktor.http.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* // other imports private val url = "https://mycompany.jetbrains.space/" private val clientId = "client-id-assigned-to-app-during-registration" private val clientSecret = "client-secret-issued-to-app-during-registration" val spaceClient by lazy { SpaceHttpClient(HttpClient(CIO)) .withServiceAccountTokenSource( clientId = clientId, clientSecret = clientSecret, serverUrl = url ) } suspend fun verifyRequestWithPublicKey(body: String, signature: String, timestamp: String): Boolean { // verify the request using a public key return spaceClient.verifyWithPublicKey(body, timestamp.toLong(), signature) } fun Routing.backToSpace() { post("/api/myapp") { // get request body val body = call.receiveText() // get hash calculated by Space val signature = call.request.header("X-Space-Public-Key-Signature") val timestamp = call.request.header("X-Space-Timestamp") val verified = signature != null && timestamp != null && verifyRequestWithPublicKey(body, signature, timestamp) if (!verified) { call.respond(HttpStatusCode.Unauthorized) return@post } // here goes your code } }

Jump to sample code

The typical workflow looks as follows:

  1. Before sending a request to your application, Space calculates a request hash.

  2. Space generates a request signature: It takes the calculated hash and encrypts it with a private key. The signature is sent along with the request in the X-Space-Public-Key-Signature HTTP header. For example, this is a sample Space request including headers:

    POST /api/myapp HTTP/1.1 X-Forwarded-For : ::ffff:123.123.123.123 X-Forwarded-Proto : https X-PageKite-Port : 443 X-Space-Timestamp : 1632844347462 X-Space-Public-Key-Signature : iOjFEi5EpW+FbU1CJl+oc0QOJbIrv7kaV/VTEMa5i0ot6418N3ObQGz2C0tB8e2N4vHKQ7LWMZZ+OJexeHHoJGvL0XN7nwWb5k7Hn1DOPMMkWgSi6kL5orkyOHqIDPaHgZzX6IGbl3LP/aPms7E1NXaFRIfxuUQGdsDeA7yCjTIv/Rq8AbV9VqVI5TudAhHmaxh6R19wgtzWxaud27rsXV8INAvwXAQLMf5Ld+E1Mzi67qrS79Jxa7U34SJjC9xDe1wYMCZGc+G+6L4zdqVB08C2MCfI1IVYzqiHLyJgCHxhBmvjJ8JivoqadbRKOrbKShvSzHqFD5geEBovO0HqKA== User-Agent : Space (81383) Ktor http-client Accept-Charset : UTF-8 Accept : */* Content-Length : 196 Content-Type : application/json Host: 12345abcdef.ngrok.io Connection : Keep-Alive {"className":"ListCommandsPayload","clientId":"f6df3d26-d9fc-41c5-9fbd-0e7896f2cfb0","userId":"2BgVYn24Jx6u"}
  3. The application receives the request. Now, it should verify the request signature.

    First, the application must obtain a public key from Space. To do this, the application must send an HTTP request to the applications/public-keys Space endpoint. For instance, this is how a request might look like:

    GET https://mycompany.jetbrains.space/api/http/applications/clientId:abc1234/public-keys Authorization: Bearer abc1234 Accept: application/json

    Here clientId is the client ID assigned to your application during registration. API Playground can help you with generating a request:

    Public keys HTTP API
  4. In the reply, Space sends a JSON Web Key set. Typically, the set contains only one key. When a current public key becomes outdated, there is a period of time when the set contains two keys (the current one and a new one).

    For each key in the set, the application must:

    1. Get the value of the X-Space-Timestamp header and the request body.

    2. Generate a string consisting of the timestamp and the body. Use colon : as a delimiter. For example in Kotlin, it can look like follows:

      val str = "$timestamp:$body"

      In case of the sample request above, the str value would be:

      1632844347462:{"className":"ListCommandsPayload", "clientId":"f6df3d26-d9fc-41c5-9fbd-0e7896f2cfb0","userId":"2BgVYn24Jx6u"}
    3. Calculate string hash using SHA512.

    4. Decrypt the content of the X-Space-Public-Key-Signature header using the RSA algorithm.

    5. Compare the decrypted signature with the calculated hash. If they are equal, the application is allowed to process the request, if not, the application must return the HTTP 401 Unauthorized response code.

      Note that only one key from the set must return the correct hash result.

Important: After a certain period of time, public keys become outdated. So, while the application can cache public keys (to avoid requesting them from Space each time), the cache should be cleared once the cached keys don't produce a correct signature. Space SDK does such caching under the hood.

Sample code

The following Kotlin code snippet demonstrates how to check the signature in the X-Space-Public-Key-Signature header:

val publicKeySignature = call.request.header("X-Space-Public-Key-Signature") val timestamp = call.request.header("X-Space-TimeStamp") ?.toLong() ?: error("Header X-Space-TimeStamp is not provided") val publicKeyString = appClient.api.applications().getPublicKeys(ApplicationIdentifier.Me) val publicKey = JWKSet.parse(publicKeyString).keys .map { it as RSAKey } .map { it.toRSAPublicKey() } .first() val rsaSignature = Signature.getInstance("SHA512withRSA") rsaSignature.initVerify(publicKey) rsaSignature.update("$timestamp:$body".toByteArray()) val signatureVerified = rsaSignature.verify(Base64.decodeBytes(publicKeySignature)) if (!signatureVerified) { // respond with 401 }

Signing key

The idea of this method is that Space uses a special signing key to generate a hash for every request it sends to your application. The calculated hash is sent in the X-Space-Signature HTTP header. In turn, your application has to use the same signing key to calculate a hash of a received request. Then it should compare the calculated hash value with the hash value in the request.

There are no helper functions in Space SDK for this method, so, our task is to implement the verification logic described in Verify Requests from Space. To calculate hash, you can use the Apache Commons Codec library. To reference it from a Gradle project, add the following lines to build.gradle:

  • To repositories:

    repositories { jcenter() // ... other repos }
  • To dependencies:

    dependencies { compile group: 'commons-codec', name: 'commons-codec', version: '1.15' // ... other dependencies }

This is how a simple implementation of this method can look like:

import org.apache.commons.codec.digest.HmacAlgorithms import org.apache.commons.codec.digest.HmacUtils // ... other imports // signing key issued during app registration val signingKey = "abc123" // calculate hash and compare it to hash from request fun verifyPayloadWithSigningKey(body: String, signature: String, timestamp: String) : Boolean { val checkedSignature = HmacUtils(HmacAlgorithms.HMAC_SHA_256, signingKey). hmacHex("$timestamp:$body") return signature == checkedSignature } fun Routing.backToSpace() { // the endpoint that handles Space requests post("api/myapp") { // get payload as text val body = call.receiveText() // get timestamp and message hash calculated by Space val signature = call.request.header("X-Space-Signature") val timestamp = call.request.header("X-Space-Timestamp") // check hash val verified = signature != null && timestamp != null && verifyPayloadWithSigningKey(body, signature, timestamp) if (!verified) { call.respond(HttpStatusCode.Unauthorized) return@post } // ... } }

The typical verification workflow looks as follows:

  1. During registration of your application, Space can issue it a signing key. To get the key, you should open the Endpoint tab of the application settings and click Generate under Signing key.

  2. Save the signing key in your application, for example, as a string constant.

  3. Before Space sends a request to your application, it calculates the request hash using the generated signing key and puts it into the X-Space-Signature header. For example, this is a sample Space request including headers:

    POST /api/myapp HTTP/1.1 Host: 12345abcdef.ngrok.io User-Agent: Space (61355) Ktor http-client Content-Length: 163 Accept: */* Accept-Charset: UTF-8 Content-Type: application/json X-Forwarded-For: 123.456.123.456 X-Forwarded-Proto: https X-Space-Signature: 2aa8cba6217a28686de0ca8dcfe2a1d0795e343d744a0c5307308e43777593a5 X-Space-Timestamp: 1607623492912 Accept-Encoding: gzip {"className":"ListCommandsPayload","accessToken":"","verificationToken":"d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3","userId":"2kawvQ4F6GM6"}
  4. Now, it's the application's turn to calculate the request hash. To do this:

    1. Get the value of the X-Space-Timestamp header and the request body.

    2. Generate a string consisting of the timestamp and the body. Use colon : as a delimiter. For example in Kotlin, it can look like follows:

      val str = "$timestamp:$body"

      In case of the sample request above, the str value would be:

      1607623492912:{"className":"ListCommandsPayload","accessToken":"","verificationToken":"d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3","userId":"2kawvQ4F6GM6"}
    3. Use HMAC SHA256 to hash the string.

  5. Compare the calculated hash with the one you get from the X-Space-Signature header. If they are equal, the application is allowed to process the request, if not, the application must return the HTTP 401 Unauthorized response code.

(Obsolete) Verification token

The idea behind the method is to compare the verification token in the request body with the request your application obtained during registration in Space. As the verification token is a part of the payload, the SDK provides an extension method for the ApplicationPayload class:

ApplicationPayload.verifyWithToken(verificationToken: String): Boolean
The method returns true if verificationToken is equal to the token in the payload.

This is how a simple implementation of this method can look like:

// token issued during app registration val verificationToken = "abc123" fun verifyPayload(payload: ApplicationPayload): Boolean { return payload.verifyWithToken(verificationToken) } fun Routing.backToSpace() { // the endpoint that handles Space requests post("api/myapp") { // get payload as text val body = call.receiveText() // deserialize payload into ApplicationPayload val payload = readPayload(body).also { if (!verifyPayload(it)) { call.respond(HttpStatusCode.Unauthorized) return@post } } // ... } }

The typical verification workflow looks as follows:

  1. During registration of your application, Space can issue it a verification token. To get the token, you should open the Endpoint tab of the application settings and click Generate under Verification token.

  2. Save this token in your application, for example, as a string constant.

  3. When Space sends a request to your application, it puts this verification token in the request body. For example, this is how the body of a slash command request looks like (a user presses / in the chatbot's channel):

    { "className": "ListCommandsPayload", "accessToken": "", "verificationToken": "d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3", "userId": "2kawvQ4F6GM6" }
  4. The task of the application is to get the verificationToken from the request payload and compare it to the token stored in the application. If they are equal, the application is allowed to process the request, if not, the application must return the HTTP 401 Unauthorized response code.

SSL client certificate

This verification method implies that all requests from Space to the application are encrypted with an SSL client key. The verification is handled not by the application but by the web server that hosts the application. Generally, to configure this verification method, you should:

  1. Generate an SSL keystore file with private and public keys.

  2. Upload the SSL keystore file to Space.

  3. During application registration, choose the uploaded keystore.

  4. On the web server that hosts your application, configure SSL client certificate authentication. For the exact instructions, refer to the web server official documentation.

HTTP authentication

This method implies using the standard HTTP authentication based on the Authorization request header. There are two ways to perform verification: using a bearer token and using basic authentication by providing a username and a password.

Space SDK doesn't provide any helper methods to perform this verification method. Most of the modern frameworks provide support for HTTP authentication out of the box. For example, here you can find the instructions for Ktor.

Bearer token

  1. During registration of your application, select HTTP Authentication and then Bearer.

  2. In Token, specify a verification token.

  3. When sending a request, Space will add this token to the Authorization header. For example:

    POST /api/myapp HTTP/1.1 Host: 12345abcdef.ngrok.io User-Agent: Space (61355) Ktor http-client X-Space-Timestamp: 1624376380652 Authorization: Bearer abc1234 X-Space-DeliveryID: e90ffc27-87dc-43f3-a13a-ac3860d53770 Accept-Charset: UTF-8 Accept: /
  4. The task of the application is to get the Authorization: Bearer header value and compare it to the token stored in the application. If they are equal, the application is allowed to process the request, if not, the application must return the HTTP 401 Unauthorized response code.

Basic authentication

  1. During registration of your application, select HTTP Authentication and then Basic.

  2. Specify a Username and a Password.

  3. Space will use the specified credentials to create a single string: username:password (a colon : is used as a separator). Then it will encode the string using the Base64 encoding.

    When sending a request, Space will add the generated string to the Authorization header. For example, for johndoe:pwd1234:

    POST /api/myapp HTTP/1.1 Host: 12345abcdef.ngrok.io User-Agent: Space (61355) Ktor http-client X-Space-Timestamp: 1624449426984 Authorization: Basic am9obmRvZTpwd2QxMjM0 X-Space-DeliveryID: 2cd74c76-9cbb-4da5-81ab-7ce578145ccc Accept-Charset: UTF-8 Accept: /

  4. The task of the application is to get the Authorization: Basic header value, decode it back to string and compare it to the credentials stored in the application. If they are equal, the application is allowed to process the request, if not, the application must return the HTTP 401 Unauthorized response code.

Last modified: 25 May 2023