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
package filters;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import io.vavr.control.Option;
import org.reactivecouchbase.json.Json;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class OtoroshiFilter implements Filter {

    private static final long JWT_VALIDATION_LEEWAY = 5000L;

    private final String mode;

    private final String sharedKey;

    private final String requestIdHeaderName;
    private final String issuer;
    private final String claimHeaderName;
    private final String stateHeaderName;
    private final String stateRespHeaderName;

    private final Logger Logger = LoggerFactory.getLogger(OtoroshiFilter.class);

    public OtoroshiFilter(String mode, String sharedKey, String issuer, String requestIdHeaderName, String claimHeaderName, String stateHeaderName, String stateRespHeaderName) {
        this.mode = mode;
        this.sharedKey = sharedKey;
        this.requestIdHeaderName = requestIdHeaderName;
        this.issuer = issuer;
        this.claimHeaderName = claimHeaderName;
        this.stateHeaderName = stateHeaderName;
        this.stateRespHeaderName = stateRespHeaderName;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        Logger.info("Filtering request for " + request.getRequestURI());
        if (mode.equalsIgnoreCase("dev")) {
            response.setHeader(stateRespHeaderName, Option.of(request.getHeader(stateHeaderName)).getOrElse("--"));
            chain.doFilter(req, res);
            return;
        } else {
            try {
                Option.of(request.getHeader(requestIdHeaderName)).forEach(id -> Logger.info("Request from Otoroshi with id : " + id + " on " + request.getRequestURI()));
                Option<String> maybeState = Option.of(request.getHeader(stateHeaderName));
                Option<String> maybeClaim = Option.of(request.getHeader(claimHeaderName));
                if (maybeClaim.isEmpty() || maybeState.isEmpty()) {
                    response.setContentType("application/json");
                    response.sendError(400, Json.obj().with("error", "Bad request ...").stringify());
                    return;
                } else {
                    if (maybeClaim.isEmpty()) {
                        response.setContentType("application/json");
                        response.setHeader(stateRespHeaderName, maybeState.get());
                        response.sendError(400, Json.obj().with("error", "Bad Claim ...").stringify());
                        return;
                    } else {
                        try {
                            Algorithm algorithm = Algorithm.HMAC512(sharedKey);
                            JWTVerifier verifier = JWT.require(algorithm)
                                    .withIssuer(issuer)
                                    .acceptLeeway(JWT_VALIDATION_LEEWAY)
                                    .build();
                            verifier.verify(maybeClaim.get());
                        } catch (UnsupportedEncodingException exception) {
                            response.setContentType("application/json");
                            response.setHeader(stateRespHeaderName, maybeState.get());
                            response.sendError(400, Json.obj().with("error", "Bad Encoding ...").stringify());
                            return;
                        } catch (JWTVerificationException exception) {
                            Logger.error("Failed to verify token: " + maybeClaim.get());
                            Logger.error("Got exception: " + exception.getMessage(), exception);
                            response.setContentType("application/json");
                            response.setHeader(stateRespHeaderName, maybeState.get());
                            response.sendError(400, Json.obj().with("error", "Bad Signature ...").stringify());
                            return;
                        }
                        response.setHeader(stateRespHeaderName, maybeState.get());
                        chain.doFilter(req, res);
                        return;
                    }
                }
            } catch (Exception e) {
                Logger.error("Error decoding jwt token", e);
                response.setContentType("application/json");
                response.sendError(400, Json.obj().with("error", e.getMessage()).stringify());
                return;
            }
        }
    }
}