Reputation: 1
I've created an integration on adobe admin. I have taken these secrets to postman and would like to now auth with OAuth 1.0 in a Kotlin application on the JVM. Any standard java libraries don't seem to put the Auth in the Authorization header like Adobe documentation dictates.
How do I do this without writing my own signer and OAuth Authorization builder?
I've created an integration on adobe admin, and I've worked with it postman. JavaScribe and Apache Signpost adds the OAuth parameters to the URL, and both are out of date by time of writing by 2 and 5 years respectively.
Upvotes: 0
Views: 35
Reputation: 1
Alright, so this answer took me on quite the adventure. While I will be sharing some honourable mentions about some gotchas while using adobe commerce rest APIs, this answer will primarily just be about the OAuth 1.0a portion.
For anyone that couldn't find the official documentation:
"message": "Specified request cannot be processed."
check your API URL. The official documentation says you need to use a "Store ID" or use the "default store" in your URL.
{domain}/rest/{storeID}/V1/{theEndpoint}
or {domain}/rest/default/V1/{theEndpoint}
. This is not necessary for all stores (or perhaps my company's store is just weird like that). My URL ended up looking like {domain}/rest/V1/{theEndpoint}
.HMAC-SHA256
for your Signature Method and that you're adding your authorization as an Authorization
header not in URL params or the request body.Authorization
header. So this was a no go for me.
getAuthorizationHeader()
. This is the library I chose for Auth Signage.
Beta
... for 12 years according to git blame.HMAC-SHA256
. However, they have a Signer class for it that I used, so perhaps the documentation just hasn't been updated.<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client</artifactId>
<version>1.38.0</version>
Someone Somewhere (probably): "But this has many transitive vulnerabilities!" Correct! Luckily I was able to remove those by excluding certain child dependencies and updating the version of one dependency. Mine is in Maven format and works for my uses, but your mileage may vary.
<!-- Updated version of the excluded guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
</dependency>
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client</artifactId>
<version>1.38.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<!-- Removed seemingly without consequence -->
<exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
</exclusions>
</dependency>
I'm also using OkHttp3 for this as HTTP client.
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
For (de)serialization I'm using Jackson.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<artifactId>jackson-core</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
<version>2.18.2</version>
</dependency>
All of this code is in Kotlin (2.1.0) but should be pretty easily transferrable to Java as necessary.
Some handy extension functions.
// LogManager comes from Log4J
val <T : Any> T.logger: Logger get() = LogManager.getLogger()
// Request is from OkHttp3
fun Request.newBuilder(
builder: Request.Builder.(Request) -> Request.Builder
) = builder(newBuilder(), this).build()
I've truncated the names since all of them live in a singleton called OAuth1, but they would work separated with more qualified names just as easily.
import com.google.api.client.auth.oauth.OAuthHmacSha256Signer
import com.google.api.client.auth.oauth.OAuthParameters
import com.google.api.client.http.GenericUrl
import okhttp3.Request
import okhttp3.Response
object OAuth1 {
// I pull secrets from a remote secrets manager so these are
// just an easy way to standardize input usage. This uses the
// nomenclature from Postman to make more straightforward to
// go from Postman to this implementation.
interface Secrets {
val consumerKey: String
val consumerSecret: String
val accessToken: String
val tokenSecret: String
}
// A default implementation data class (not necessary to use).
data class DefaultSecrets(
override val consumerKey: String = "consumerKey",
override val consumerSecret: String = "consumerSecret",
override val accessToken: String = "accessToken",
override val tokenSecret: String = "tokenSecret"
) : Secrets
// Implementing this interface on a class should make it very easy to
// integrate OAuth1 in the class for usage.
interface UsesOAuth1 {
val oAuthSecrets: Secrets
// This is where the HMAC-SHA256 comes into play.
// Change this for different encryption types.
val signer
get() = OAuthHmacSha256Signer(oAuthSecrets.consumerSecret)
.apply { this.setTokenSecret(oAuthSecrets.tokenSecret) }
val oauthParams: OAuthParameters
get() = OAuthParameters().apply {
[email protected] = oAuthSecrets.consumerKey
[email protected] = oAuthSecrets.accessToken
[email protected] = [email protected]
}
}
// An OkHttp3 Interceptor that adds the Authorization header to each call
// created with a client it is added to.
class Interceptor(
private val oauthParams: OAuthParameters
) : okhttp3.Interceptor {
private fun Request.computeOAuth() {
oauthParams.run {
// Nonce, Timestamp, and signature need to be unique for each
// call and is therefore generated per intercept.
computeTimestamp()
computeNonce()
computeSignature(method, GenericUrl(url.toString()))
}
}
override fun intercept(
chain: okhttp3.Interceptor.Chain
): Response = chain.request().newBuilder {
logger.info("Calling with OAuth1: ${it.method}: ${it.url}")
it.computeOAuth()
addHeader("Authorization", oauthParams.getAuthorizationHeader())
}.let {
chain.proceed(it)
}
}
}
So that's all the OAuth stuff at a high level. However, I'll go ahead and share the prototype implementation I have with he HTTP client for anyone that just wants a copy, paste, and go.
Some more handy extension functions.
// HttpUrl is from OkHttp3
fun HttpUrl.newBuilder(
builder: HttpUrl.Builder.() -> HttpUrl.Builder
) = builder(newBuilder()).build()
inline fun <reified T> String.tryParseAsJson(): T? =
runCatching { parseAsJson<T>() }.onFailure {
val type = T::class.simpleName?.let { " ($it)" } ?: ""
logger.warn("Failed to parse JSON$type. String: $this")
}.getOrNull()
inline fun <reified T> String.parseAsJson(): T =
jacksonObjectMapper.readValue(this, T::class.java)
val jacksonObjectMapper: ObjectMapper by lazy {
ObjectMapper()
.registerModule(jacksonKotlinModule)
.findAndRegisterModules()
}
private val jacksonKotlinModule by lazy {
KotlinModule.Builder()
.withReflectionCacheSize(512)
.configure(KotlinFeature.NullToEmptyCollection, false)
.configure(KotlinFeature.NullToEmptyMap, false)
.configure(KotlinFeature.NullIsSameAsDefault, false)
.configure(KotlinFeature.SingletonSupport, false)
.configure(KotlinFeature.StrictNullChecks, false)
.build()
}
Below is the abstract base API client.
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
abstract class BaseApiClient {
abstract val baseUrl: HttpUrl
abstract val httpClient: OkHttpClient
abstract fun handler(response: Response)
inline fun <reified T> Request.executeWithHandling(): T? {
val response = httpClient.newCall(this).execute()
if (!response.isSuccessful) handler(response)
// For now this will just parse JSON.
// However, in the future this could use
// body.mediaType to dynamically parse the T type.
return response.body?.string()?.tryParseAsJson<T>()
}
fun String.get(): Request = Request.Builder().apply {
[email protected]()
url(baseUrl.newBuilder { addPathSegments(this@get) })
}.build()
}
Below is an implementation of the BaseApiClient
into a "MagentoApiClient" specific to adobe commerce. Note, this only has a single endpoint and the baseUrl
has been scrubbed for confidentiality reasons.
class MagentoApiClient(
override val oAuthSecrets: OAuth1.Secrets,
override val baseUrl: HttpUrl = "https://#{yourStoreDomain}/rest/V1/".toHttpUrl(),
) : BaseApiClient(), OAuth1.UsesOAuth1 {
override val httpClient: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(OAuth1.Interceptor(oauthParams))
.build()
override fun handler(response: Response) {
// The only reason this function would be called is if the response code is not 2xx or 3xx.
response.body?.string()?.let { error ->
MagentoApiExceptionHandler.handle(error)
}
}
fun customerById(id: Int): MagentoCustomer? =
"customers/$id".get().executeWithHandling()
}
Usage:
fun main() {
// Please don't commit your secrets to source code,
// and please make sure to pull them from a secure source.
val magentoApi = MagentoApiClient(OAuth1.DefaultSecrets())
val response = magentoApi.customerById(15)
println(response)
}
Alas I'm unable to share the data model for MagentoCustomer
or the exception handler code. However, this should be able to get you up and running from almost nothing. Feel free to throw any questions in my general direction, and I'll answer as much as I can.
Upvotes: 0