The Otoroshi communication protocol
The exchange protocol secure the communication with an app. When it’s enabled, Otoroshi will send for each request a value in pre-selected token header, and will check the same header in the return request. On routes, you will have to use the Otoroshi challenge token
plugin to enable it.
V1 challenge
If you enable secure communication for a given service with V1 - simple values exchange
activated, you will have to add a filter on the target application that will take the Otoroshi-State
header and return it in a header named Otoroshi-State-Resp
.
you can find an example project that implements V1 challenge here
V2 challenge
If you enable secure communication for a given service with V2 - signed JWT token exhange
activated, you will have to add a filter on the target application that will take the Otoroshi-State
header value containing a JWT token, verify it’s content signature then extract a claim named state
and return a new JWT token in a header named Otoroshi-State-Resp
with the state
value in a claim named state-resp
. By default, the signature algorithm is HMAC+SHA512 but can you can choose your own. The sent and returned JWT tokens have short TTL to avoid being replayed. You must be validate the tokens TTL. The audience of the response token must be Otoroshi
and you have to specify iat
, nbf
and exp
.
you can find an example project that implements V2 challenge here
Info. token
Otoroshi is also sending a JWT token in a header named Otoroshi-Claim
that the target app can validate too. On routes, you will have to use the Otoroshi info. token
plugin to enable it.
The Otoroshi-Claim
is a JWT token containing some informations about the service that is called and the client if available. You can choose between a legacy version of the token and a new one that is more clear and structured.
By default, the otoroshi jwt token is signed with the otoroshi.claim.sharedKey
config property (or using the $CLAIM_SHAREDKEY
env. variable) and uses the HMAC512
signing algorythm. But it is possible to customize how the token is signed from the service descriptor page in the Otoroshi exchange protocol
section.
using another signing algo.
here you can choose the signing algorithm and the secret/keys used. You can use syntax like ${env.MY_ENV_VAR}
or ${config.my.config.path}
to provide secret/keys values.
For example, for a service named my-service
with a signing key secret
with HMAC512
signing algorythm, the basic JWT token that will be sent should look like the following
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiItLSIsImF1ZCI6Im15LXNlcnZpY2UiLCJpc3MiOiJPdG9yb3NoaSIsImV4cCI6MTUyMTQ0OTkwNiwiaWF0IjoxNTIxNDQ5ODc2LCJqdGkiOiI3MTAyNWNjMTktMmFjNy00Yjk3LTljYzctMWM0ODEzYmM1OTI0In0.mRcfuFVFPLUV1FWHyL6rLHIJIu0KEpBkKQCk5xh-_cBt9cb6uD6enynDU0H1X2VpW5-bFxWCy4U4V78CbAQv4g
if you decode it, the payload will look something like
{
"sub": "apikey_client_id",
"aud": "my-service",
"iss": "Otoroshi",
"exp": 1521449906,
"iat": 1521449876,
"jti": "71025cc19-2ac7-4b97-9cc7-1c4813bc5924"
}
If you want to validate the Otoroshi-Claim
on the target app side to ensure that the input requests only comes from Otoroshi
, you will have to write an HTTP filter to do the job. For instance, if you want to write a filter to make sure that requests only comes from Otoroshi, you can write something like the following (using playframework 2.6).
- Scala
-
import akka.stream.Materializer import com.auth0.jwt._ import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.interfaces._ import play.api.Logger import play.api.libs.json._ import play.api.libs.typedmap._ import play.api.mvc._ import scala.concurrent.{ExecutionContext, Future} import scala.util._ object OtoroshiFilter { object Attrs { val OtoroshiClaim: TypedKey[DecodedJWT] = TypedKey("otoroshi-claim") } } class OtoroshiFilter(env: String, sharedKey: String)(implicit ec: ExecutionContext, val mat: Materializer) extends Filter { def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = { val maybeState = requestHeader.headers.get("Otoroshi-State") val maybeClaim = requestHeader.headers.get("Otoroshi-Claim") env match { case "dev" => nextFilter(requestHeader).map { result => result.withHeaders( "Otoroshi-State-Resp" -> maybeState.getOrElse("--") ) } case "prod" if maybeClaim.isEmpty && maybeState.isEmpty => Future.successful( Results.Unauthorized( Json.obj("error" -> "Bad request !!!") ) ) case "prod" if maybeClaim.isEmpty => Future.successful( Results.Unauthorized( Json.obj("error" -> "Bad claim !!!") ).withHeaders( "Otoroshi-State-Resp" -> maybeState.getOrElse("--") ) ) case "prod" => Try { val algorithm = Algorithm.HMAC512(sharedKey) val verifier = JWT .require(algorithm) .withIssuer("Otoroshi") .acceptLeeway(5000) .build() val decoded = verifier.verify(maybeClaim.get) nextFilter(requestHeader.addAttr(OtoroshiFilter.Attrs.OtoroshiClaim, decoded)).map { result => result.withHeaders( "Otoroshi-State-Resp" -> maybeState.getOrElse("--") ) } } recoverWith { case e => Success( Future.successful( Results.Unauthorized( Json.obj("error" -> "Claim error !!!", "m" -> e.getMessage) ).withHeaders( "Otoroshi-State-Resp" -> maybeState.getOrElse("--") ) ) ) } get case _ => Future.successful( Results.Unauthorized( Json.obj("error" -> "Bad env !!!") ).withHeaders( "Otoroshi-State-Resp" -> maybeState.getOrElse("--") ) ) } } }
- Java