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 Enable client credentials flow in the Authentications 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 Enable code flow and specify the redirect URI in Code Flow 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:
Use the Ktor's Authentication feature for obtaining an access token.
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