All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.toolsplus.atlassian.jwt.HttpRequestCanonicalizer.scala Maven / Gradle / Ivy

The newest version!
package io.toolsplus.atlassian.jwt

import java.io.UnsupportedEncodingException
import java.net.URLEncoder
import java.security.{MessageDigest, NoSuchAlgorithmException}

import io.toolsplus.atlassian.jwt.api.CanonicalHttpRequest
import org.bouncycastle.util.encoders.Hex

/**
  * Instructions for computing the query hash parameter ("qsh") from a HTTP request.
  * -------------------------------------------------------------------------------------
  *
  * Overview:       query hash = hash(canonical-request)
  *
  * canonical-request = canonical-method + '&' + canonical-URI + '&' + canonical-query-string
  *
  * 1. Compute canonical method.
  * Simply the upper-case of the method name (e.g. "GET", "PUT").
  *
  * 2. Append the character '&'
  *
  * 3. Compute canonical URI.
  * Discard the protocol, server, port, context path and query parameters from the full URL.
  * For requests targeting add-ons discard the `baseUrl` in the add-on descriptor.
  * (Removing the context path allows a reverse proxy to redirect incoming requests for "jira.example.com/getsomething"
  * to "example.com/jira/getsomething" without breaking authentication. The requester cannot know that the reverse proxy
  * will prepend the context path "/jira" to the originally requested path "/getsomething".)
  * Empty-string is not permitted; use "/" instead.
  * Do not suffix with a '/' character unless it is the only character.
  * Url-encode any '&' characters in the path.
  *       E.g. in "http://server:80/some/path/?param=value" the canonical URI is "/some/path"
  * and in "http://server:80" the canonical URI is "/".
  *
  * 4. Append the character '&'.
  *
  * 5. Compute the canonical query string.
  * Sort the query parameters primarily by their percent-encoded names and secondarily by their percent-encoded values.
  * Sorting is by codepoint: sort(["a", "A", "b", "B"]) => ["A", "B", "a", "b"].
  * For each parameter append its percent-encoded name, the '=' character and then its percent-encoded value.
  * In the case of repeated parameters append the ',' character and subsequent percent-encoded values.
  * Ignore the JWT query string parameter, if present.
  * Some particular values to be aware of: "+" is encoded as "%20",
  * "*" as "%2A" and
  * "~" as "~".
  * (These values used for consistency with OAuth1.)
  * An example: for a GET request to the not-yet-percent-encoded URL
  * "http://localhost:2990/path/to/service?zee_last=param&repeated=parameter 1&first=param&
  * repeated=parameter 2"
  * the canonical request is "GET&/path/to/service&first=param&repeated=parameter%201,parameter%202&
  * zee_last=param".
  *
  * 6. Convert the canonical request string to bytes.
  * The encoding used to represent characters as bytes is UTF-8.
  *
  * 7. Hash the canonical request bytes using the SHA-256 algorithm.
  *    E.g. The SHA-256 hash of "foo" is "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae".
  */
object HttpRequestCanonicalizer {

  val QueryStringHashClaimName: String = "qsh"

  /**
    * When the JWT message is specified in the query string of a URL then this is the parameter name.
    *
    * E.g. "jwt" in:
    * 
    * http://server:80/some/path?otherparam=value&jwt=eyJhbGciOiJIUzI1NiIsI.eyJleHAiOjEzNzg5NCI6MTM3ODk1MjQ4OH0
    * .cDihfcsKW_We_EY21tIs55dVwjU
    * 
*/ private val JwtParamName: String = "jwt" /** * Query parameter separator as it appears between "value1" and "param2" in the URL * "http://server/path?param1=value1&param2=value2". */ private val QueryParamsSeparator: Char = '&' /** * The character between "a" and "b%20c" in "some_param=a,b%20c" */ private val EncodedParamValueSeparator: String = "," /** * For separating the method, URI etc in a canonical request string. */ private[jwt] val CanonicalRequestPartSeparator: Char = '&' /** * Assemble the components of the HTTP request into the correct format so that they can be signed or hashed. * * @param request [[CanonicalHttpRequest]] that provides the necessary components * @return String encoding the canonical form of this request as required for constructing query string hash values * @throws UnsupportedEncodingException [[UnsupportedEncodingException]] if the [[java.net.URLEncoder]] cannot encode the request's field's characters */ def canonicalize(request: CanonicalHttpRequest): String = s"${canonicalizeMethod(request)}$CanonicalRequestPartSeparator" + s"${canonicalizeUri(request)}$CanonicalRequestPartSeparator" + s"${canonicalizeQueryParameters(request)}" /** * Canonicalize the given [[CanonicalHttpRequest]] and hash it. * This request hash can be included as a JWT claim to verify that request components are genuine. * * @param request CanonicalHttpRequest to be canonicalized and hashed * @return String hash suitable for use as a JWT claim value * @throws UnsupportedEncodingException if the [[java.net.URLEncoder]] cannot encode the request's field's characters * @throws NoSuchAlgorithmException if the hashing algorithm does not exist at runtime */ def computeCanonicalRequestHash(request: CanonicalHttpRequest): String = { // prevent the code in this method being repeated in every call site that needs a request hash, // encapsulate the knowledge of the type of hash that we are using computeSha256Hash(canonicalize(request)) } /** * Compute the SHA-256 hash of hashInput. * E.g. The SHA-256 hash of "foo" is "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae". * * @param hashInput String to be hashed. * @return String hash * @throws NoSuchAlgorithmException if the hashing algorithm does not exist at runtime */ private def computeSha256Hash(hashInput: String): String = { val digest = MessageDigest.getInstance("SHA-256") val hashInputBytes = hashInput.getBytes digest.update(hashInputBytes, 0, hashInputBytes.length) new String(Hex.encode(digest.digest())) } private[jwt] def canonicalizeMethod(request: CanonicalHttpRequest): String = request.method.toUpperCase private[jwt] def canonicalizeUri(request: CanonicalHttpRequest): String = { val relativeRequestPath = request.relativePath val pathWithoutTrailingSlash = if (relativeRequestPath.endsWith("/")) relativeRequestPath.dropRight(1) else relativeRequestPath val path = if (pathWithoutTrailingSlash.isEmpty) "/" else pathWithoutTrailingSlash val separatorAsString = CanonicalRequestPartSeparator.toString // If the separator is not URL encoded then the following URLs have the same query-string-hash: // https://djtest9.jira-dev.com/rest/api/2/project&a=b?x=y // https://djtest9.jira-dev.com/rest/api/2/project?a=b&x=y val encodedPath = path.replaceAll(separatorAsString, percentEncode(separatorAsString)) if (encodedPath.startsWith("/")) encodedPath else s"/$encodedPath" } private[jwt] def canonicalizeQueryParameters( request: CanonicalHttpRequest): String = { (request.parameterMap - JwtParamName).toSeq .sortBy { case (key, values) => s"${percentEncode(key)} ${percentEncode(values.mkString(","))}" } .map((percentEncodePair _).tupled) .mkString(QueryParamsSeparator.toString) } /** * Construct a form-urlencoded document from the given name/parameter pair. */ private def percentEncodePair(key: String, values: Seq[String]): String = { val encKey = percentEncode(key) val encVal = values.map(percentEncode).mkString(EncodedParamValueSeparator) s"$encKey=$encVal" } /** * Encode value using URLEncoder.encode() but encode some characters differently to URLEncoder, to match OAuth1 * and VisualVault. * * @param value String to be percent-encoded * @return encoded Encoded result string * @throws UnsupportedEncodingException if URLEncoder does not support UTF-8 */ private def percentEncode(value: String): String = URLEncoder .encode(value, "UTF-8") .replace("+", "%20") .replace("*", "%2A") .replace("%7E", "~") }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy