How to implement Resource Owner Password Credentials to access Microsoft Graph API

Ondrej Kvasnovsky
3 min readFeb 15, 2024

--

Here is more context what ROPC (Resource Owner Password Credentials) is: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc

Gradle setup (Kotlin)

We need to add dependencies for Microsoft APIs, and also add dependency and install plugin for Kotlin serialization, which will enable us to parse string payload that we get from the Microsoft API into TokenResponse.

plugins {
...
kotlin("plugin.serialization") version "1.9.21"
...
}

dependencies {
...
implementation("com.azure:azure-identity:1.10.4")
implementation("com.microsoft.graph:microsoft-graph:5.75.0")

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
...
}

TokenResponse

Here is a data class we will use to parse the response from Microsoft Graph Auth API.

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class TokenResponse(
@SerialName("token_type") val tokenType: String,
@SerialName("scope") val scope: String,
@SerialName("expires_in") val expiresIn: Int,
@SerialName("ext_expires_in") val extExpiresIn: Int,
@SerialName("access_token") val accessToken: String,
)

ROPCTokenCredentials using OkHTTP3

The first implementation is using https://square.github.io/okhttp.

import com.azure.core.credential.AccessToken
import com.azure.core.credential.TokenCredential
import com.azure.core.credential.TokenRequestContext
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import reactor.core.publisher.Mono
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset

/**
* https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc
*/
class ROPCTokenCredential(
private var clientId: String,
private var tenantId: String,
private var clientSecret: String,
private var username: String,
private var password: String,
private var scope: String = "https://graph.microsoft.com/.default",
) : TokenCredential {

override fun getToken(context: TokenRequestContext?): Mono<AccessToken> {
return Mono.defer {
val authToken = fetchAuthToken()
if (authToken == null) {
Mono.error(RuntimeException("Failed to obtain access token"))
} else {
val currentInstant = Instant.now()
val expirationInstant = currentInstant.plusSeconds(authToken.expiresIn.toLong())
val expirationDateTime = OffsetDateTime.ofInstant(expirationInstant, ZoneOffset.UTC)
Mono.just(AccessToken(authToken.accessToken, expirationDateTime))
}
}
}

fun fetchAuthToken(): TokenResponse? {
try {
val url = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"

val formBody = FormBody.Builder()
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("grant_type", "password")
.add("username", username)
.add("password", password)
.add("scope", scope)
.build()

val request = Request.Builder()
.url(url)
.post(formBody)
.build()

val client = OkHttpClient()

client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
val responseBody = response.body?.string()
println("Response: $responseBody")
val tokenResponse = Json.decodeFromString<TokenResponse>(responseBody!!)

println("Access Token: ${tokenResponse.accessToken}")
println("Token Type: ${tokenResponse.tokenType}")
println("Expires In: ${tokenResponse.expiresIn}")

return tokenResponse
} else {
println("Error: ${response.code} - ${response.body?.string()}")
return null
}
}
} catch (e: Exception) {
throw RuntimeException("Failed to obtain access token")
}
}
}

ROPCTokenCredentials using RestTemplate

This implementation is using RestTemplate, which makes is it a bit easier to unit test.

import com.azure.core.credential.AccessToken
import com.azure.core.credential.TokenCredential
import com.azure.core.credential.TokenRequestContext
import kotlinx.serialization.json.Json
import org.springframework.http.*
import org.springframework.web.client.RestTemplate
import reactor.core.publisher.Mono
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset

/**
* https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc
*/
class ROPCTokenCredential(
private var clientId: String,
private var tenantId: String,
private var clientSecret: String,
private var username: String,
private var password: String,
private var scope: String = "https://graph.microsoft.com/.default",
private val restTemplate: RestTemplate = RestTemplate()
) : TokenCredential {

override fun getToken(context: TokenRequestContext?): Mono<AccessToken> {
return Mono.defer {
val authToken = fetchAuthToken()
if (authToken == null) {
Mono.error(RuntimeException("Failed to obtain access token"))
} else {
val currentInstant = Instant.now()
val expirationInstant = currentInstant.plusSeconds(authToken.expiresIn.toLong())
val expirationDateTime = OffsetDateTime.ofInstant(expirationInstant, ZoneOffset.UTC)
Mono.just(AccessToken(authToken.accessToken, expirationDateTime))
}
}
}

fun fetchAuthToken(): TokenResponse? {
try {
val url = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"

val headers = HttpHeaders()
headers.contentType = MediaType.APPLICATION_FORM_URLENCODED

val formBody = mapOf(
"client_id" to clientId,
"client_secret" to clientSecret,
"grant_type" to "password",
"username" to username,
"password" to password,
"scope" to scope
)

val requestEntity = HttpEntity(buildFormUrlEncodedBody(formBody), headers)

val responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String::class.java)

if (responseEntity.statusCode.is2xxSuccessful) {
val responseBody = responseEntity.body
println("Response: $responseBody")
val tokenResponse = Json.decodeFromString<TokenResponse>(responseBody!!)

println("Access Token: ${tokenResponse.accessToken}")
println("Token Type: ${tokenResponse.tokenType}")
println("Expires In: ${tokenResponse.expiresIn}")

return tokenResponse
} else {
println("Error: ${responseEntity.statusCode} - ${responseEntity.body}")
return null
}
} catch (e: Exception) {
throw RuntimeException("Failed to obtain access token")
}
}

private fun buildFormUrlEncodedBody(formData: Map<String, String>): String {
return formData.entries.joinToString("&") { "${it.key}=${it.value}" }
}
}

Using ROPCTokenCredential

Now we can use the token credentials in our, e.g. Spring, application. The properties are externalized into application.properties file.

import com.azure.core.credential.TokenCredential
import com.microsoft.graph.authentication.TokenCredentialAuthProvider
import com.microsoft.graph.requests.GraphServiceClient
import io.github.oshai.kotlinlogging.KotlinLogging
import okhttp3.Request
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.Bean

@ConfigurationPropertiesScan
@ConfigurationProperties(prefix = "graph-api")
class GraphApiConfig(
private val clientId: String,
private val tenantId: String,
private val clientSecret: String,
private val username: String,
private val password: String,
) {

@Bean
fun clientSecretCredential(): TokenCredential {
return ROPCTokenCredential(
clientId,
tenantId,
clientSecret,
username,
password,
)
}

@Bean
fun tokenCredentialAuthProvider(): TokenCredentialAuthProvider {
return TokenCredentialAuthProvider(
listOf("https://graph.microsoft.com/.default"), clientSecretCredential() // + scopes.split(",")
)
}

@Bean
fun graphServiceClient(): GraphServiceClient<Request> {
return GraphServiceClient.builder()
.authenticationProvider(tokenCredentialAuthProvider())
.buildClient()
}
}

Here is what is expected in the application.properties file:

graph-api.clientId=
graph-api.tenantId=
graph-api.clientSecret=z
graph-api.username=
graph-api.password=

References

--

--