JetBrains Space Help

Authenticate and Authorize

Communication with Space is handled by the SpaceHttpClient base class. Before you can use it to send requests to Space, you must obtain an authentication token.

Obtaining an access token

The way you obtain the access token depends on the authentication flow you choose. The SpaceHttpClient class provides several extension methods that simplify authentication for some of the flows. See the examples below.

Client Credentials flow

The Client Credentials flow is used to authorize the application on behalf of itself. In this flow, the application receives an access token from Space by sending it client_id and client_secret. When registering the application, you should select Client Credentials Flow in the Authentication tab.

The SpaceHttpClient class provides the withServiceAccountTokenSource method for working with the Client Credentials flow. For example:

// URL of your Space instance const val spaceUrl = "https://mycompany.jetbrains.space" // 'clientId' and 'clientSecret' are issued when you // [[[register the application|https://www.jetbrains.com/help/space/register-app-in-space.html#specify-authentication-options]]] in Space val clientId = System.getenv("JB_SPACE_CLIENT_ID") val clientSecret = System.getenv("JB_SPACE_CLIENT_SECRET") // Create a base Space client val baseClient = SpaceHttpClient(HttpClient()) // Make a request. // The "**" arg defines the [[[scope|https://www.jetbrains.com/help/space/authentication-a.html#scopes]]] val spaceClient = baseClient.withServiceAccountTokenSource( clientId, clientSecret, spaceUrl, "**") val absences = spaceClient.absences.getAllAbsences()

Authorization Code flow

The Authorization Code flow is used to authorize the application on behalf of a user. In this flow, the application sends a user to Space via a link. After the user logs in to Space, Space redirects the user back to the application using the specified redirect URI. The redirect also contains an authorization code. The application uses the authorization code to obtain an access token from Space. When registering the application, you should select Authorization Code Flow and specify the Redirect URIs. Note that if the Authorization code flow is enabled, Space automatically enables the Refresh Token flow: the issued access token is valid only for 600 seconds, after this your application must obtain a new one.

There is a number of ways to implement the Authorization Code flow in your application. One of the ways is to:

  1. Use the Ktor's Authentication feature for obtaining an access token.

  2. Use the withPermanentToken method of the SpaceHttpClient class to access Space endpoints with the obtained token.

For example, the following application logs in to Space on behalf of a user and shows the Hello {username}! message on its index page:

package space.auth.example import io.ktor.application.* import io.ktor.response.* import io.ktor.request.* import io.ktor.routing.* import io.ktor.http.* import io.ktor.html.* import kotlinx.html.* import io.ktor.auth.* import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.features.* import io.ktor.sessions.* import kotlinx.datetime.Clock import space.jetbrains.api.runtime.* import space.jetbrains.api.runtime.resources.teamDirectory import space.jetbrains.api.runtime.resources.todoItems import space.jetbrains.api.runtime.types.ProfileIdentifier import kotlin.time.ExperimentalTime import kotlin.time.seconds const val spaceUrl = "https://mycompany.jetbrains.space" // base Space client val baseClient = SpaceHttpClient(HttpClient()) // Space access token var spaceToken: ExpiringToken? = null // check token expiration fun TokenInfo.expired(): Boolean { return if (this.expires != null) { (Clock.System.now() > this.expires!!) } else true } ` // OAuth provider val spaceOauthProvider = OAuthServerSettings.OAuth2ServerSettings( name = "Space", authorizeUrl = "$spaceUrl/oauth/auth", accessTokenUrl = "$spaceUrl/oauth/token", requestMethod = HttpMethod.Post, // 'clientId' and 'clientSecret' are generated when you // [[[register the application|https://www.jetbrains.com/help/space/register-app-in-space.html#specify-authentication-options]]] in Space // Do not store id and secret in plain text! clientId = System.getenv("JB_SPACE_CLIENT_ID"), clientSecret = System.getenv("JB_SPACE_CLIENT_SECRET"), // list of [[[scopes|https://www.jetbrains.com/help/space/authentication-a.html#scopes]]] defaultScopes = listOf("**"), ) fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @ExperimentalTime @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { install(Authentication) { // we need OAuth 2.0 flows oauth("space-oauth") { // it requires a client to perform auth flow client = HttpClient() // provider with OAuth server settings providerLookup = { spaceOauthProvider } // redirect that will return user back to the app urlProvider = { redirectUrl("/login") } } } routing { // index page get("/") { // If token is not valid, log in to Space if (spaceToken == null || spaceToken!!.expired()) { call.respondRedirect("/login") return@get } // If token is valid, get username from Space val spaceClient = baseClient.withPermanentToken( spaceToken!!.accessToken, spaceUrl) val profile = spaceClient.teamDirectory.profiles.getProfile( ProfileIdentifier.Me) val username = profile.username // and return HTML page with greeting val msg = "Hello ${username}!" call.respondHtml { head { title(msg) } body { p { +msg } } } } authenticate("space-oauth") { // login page route("/login") { handle { // Get the OAuth principal (incl. the token). // Ktor Authentication feature performs the // authentication code flow by default val principal = call.authentication. principal<OAuthAccessTokenResponse.OAuth2>() ?: error("No principal") // Get token val token = principal.accessToken val expires = (Clock.System.now() + principal.expiresIn.seconds) spaceToken = ExpiringToken(token, expires) call.respondRedirect("/") } } } } } // generate redirect URLs private fun ApplicationCall.redirectUrl(path: String): String { val host = request.host() return "https://$host$path" }

If you don't want to use the Ktor Authentication and Session features, you can implement the Authorization Code and Refresh Token flows by yourself. In this case, you can use the withCallContext() method of the SpaceHttpClient class to communicate with Space and obtain new tokens. For example:

// URL of your Space instance const val spaceUrl = "https://mycompany.jetbrains.space" // Create a base Space client val baseClient = SpaceHttpClient(HttpClient()) val spaceClient = baseClient.withCallContext( SpaceHttpClientCallContext(spaceUrl, ExpiringTokenSource { // This function will run once the token expires obtainToken() } )) fun obtainToken(): ExpiringToken { // TODO: Obtain a token from Space }

Personal token

If you authorize the application in Space with a personal token, you already have a permanent access token. To access Space endpoints, use the withPermanentToken method of the SpaceHttpClient class. For example:

// URL of your Space instance const val spaceUrl = "https://mycompany.jetbrains.space" // Personal token val token = System.getenv("JB_SPACE_TOKEN") // Create a base Space client val baseClient = SpaceHttpClient(HttpClient()) // Make a request val spaceClient = baseClient.withPermanentToken(token, spaceUrl) val absences = spaceClient.absences.getAllAbsences()

Scopes

Scope is a mechanism in OAuth 2.0 to limit an application's access to a user account. By default, Space API client uses the ** scope, which requests all rights available to the application. The list of rights available to the application is defined during application registration.

There is a number of ways to specify the required scope depending on your authentication flow. For example:

  • If you set up a SpaceHttpClient instance using the withServiceAccountTokenSource extension method, use the scope parameter:

    const val spaceUrl = "https://mycompany.jetbrains.space" val baseClient = SpaceHttpClient(HttpClient()) // request the right to 'View absences' val scope = "Profile:ViewAbsences" val spaceClient = baseClient.withServiceAccountTokenSource( clientId, clientSecret, spaceUrl, scope)

  • If you use the Ktor's Authentication feature to obtain a token from Space, define the scope in the OAuth settings provider (see the full example ):

    val spaceOAuthProvider = OAuthServerSettings.OAuth2ServerSettings( name = "space-oauth", authorizeUrl = "$spaceUrl/oauth/auth", accessTokenUrl = "$spaceUrl/oauth/token", requestMethod = HttpMethod.Post, accessTokenRequiresBasicAuth = true, clientId = System.getenv("JB_SPACE_CLIENT_ID"), clientSecret = System.getenv("JB_SPACE_CLIENT_SECRET"), // request the right to 'View absences' defaultScopes = listOf("Profile:ViewAbsences") )

If the application does not have the right requested in the scope, Space will return HTTP ERROR 500 with error details like this:

{ "error":"permission-denied", "error_description":"Permission is not granted: View member profile" }

To find out what scope is required for a certain HTTP API call

  1. Open the HTTP API Playground.

  2. Find and select the required endpoint. The required scope will be shown on the top of the page:

    Check rights in API Playground

Last modified: 15 June 2021