diff --git a/obp-api/src/main/scala/code/api/directlogin.scala b/obp-api/src/main/scala/code/api/directlogin.scala index 77a1668d5..8d67dcc9f 100644 --- a/obp-api/src/main/scala/code/api/directlogin.scala +++ b/obp-api/src/main/scala/code/api/directlogin.scala @@ -416,6 +416,69 @@ object DirectLogin extends RestHelper with MdcLoggable { } + /** + * Validator that uses pre-extracted parameters from CallContext (for http4s support) + * This avoids dependency on S.request which is not available in http4s context + */ + def validatorFutureWithParams(requestType: String, httpMethod: String, parameters: Map[String, String]): Future[(Int, String, Map[String, String])] = { + + def validAccessTokenFuture(tokenKey: String) = { + Tokens.tokens.vend.getTokenByKeyAndTypeFuture(tokenKey, TokenType.Access) map { + case Full(token) => token.isValid + case _ => false + } + } + + var message = "" + var httpCode: Int = 500 + + val missingParams = missingDirectLoginParameters(parameters, requestType) + val validParams = validDirectLoginParameters(parameters) + + val validF = + if (requestType == "protectedResource") { + validAccessTokenFuture(parameters.getOrElse("token", "")) + } else if (requestType == "authorizationToken" && + APIUtil.getPropsAsBoolValue("direct_login_consumer_key_mandatory", true)) { + APIUtil.registeredApplicationFuture(parameters.getOrElse("consumer_key", "")) + } else { + Future { true } + } + + for { + valid <- validF + } yield { + if (parameters.get("error").isDefined) { + message = parameters.get("error").getOrElse("") + httpCode = 400 + } + else if (missingParams.nonEmpty) { + message = ErrorMessages.DirectLoginMissingParameters + missingParams.mkString(", ") + httpCode = 400 + } + else if (SILENCE_IS_GOLDEN != validParams.mkString("")) { + message = validParams.mkString("") + httpCode = 400 + } + else if (requestType == "protectedResource" && !valid) { + message = ErrorMessages.DirectLoginInvalidToken + parameters.getOrElse("token", "") + httpCode = 401 + } + else if (requestType == "authorizationToken" && + APIUtil.getPropsAsBoolValue("direct_login_consumer_key_mandatory", true) && + !valid) { + logger.error("application: " + parameters.getOrElse("consumer_key", "") + " not found") + message = ErrorMessages.InvalidConsumerKey + httpCode = 401 + } + else + httpCode = 200 + if (message.nonEmpty) + logger.error("error message : " + message) + (httpCode, message, parameters) + } + } + private def generateTokenAndSecret(claims: JWTClaimsSet): (String, String) = { // generate random string @@ -473,12 +536,20 @@ object DirectLogin extends RestHelper with MdcLoggable { } def getUserFromDirectLoginHeaderFuture(sc: CallContext) : Future[(Box[User], Option[CallContext])] = { - val httpMethod = S.request match { + val httpMethod = if (sc.verb.nonEmpty) sc.verb else S.request match { case Full(r) => r.request.method case _ => "GET" } + // Prefer directLoginParams from CallContext (http4s), fall back to S.request (Lift) + val directLoginParamsFromCC = sc.directLoginParams for { - (httpCode, message, directLoginParameters) <- validatorFuture("protectedResource", httpMethod) + (httpCode, message, directLoginParameters) <- if (directLoginParamsFromCC.nonEmpty && directLoginParamsFromCC.contains("token")) { + // Use params from CallContext (http4s path) + validatorFutureWithParams("protectedResource", httpMethod, directLoginParamsFromCC) + } else { + // Fall back to S.request (Lift path) + validatorFuture("protectedResource", httpMethod) + } _ <- Future { if (httpCode == 400 || httpCode == 401) Empty else Full("ok") } map { x => fullBoxOrException(x ?~! message) } consumer <- OAuthHandshake.getConsumerFromTokenFuture(200, (if (directLoginParameters.isDefinedAt("token")) directLoginParameters.get("token") else Empty)) user <- OAuthHandshake.getUserFromTokenFuture(200, (if (directLoginParameters.isDefinedAt("token")) directLoginParameters.get("token") else Empty)) diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index 4e063318a..3b686ed69 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -149,22 +149,50 @@ object Http4sCallContextBuilder { /** * Extract DirectLogin header parameters if present - * DirectLogin header format: DirectLogin token="xxx" + * Supports two formats: + * 1. New format (2021): DirectLogin: token=xxx + * 2. Old format (deprecated): Authorization: DirectLogin token=xxx */ private def extractDirectLoginParams(request: Request[IO]): Map[String, String] = { + // Try new format first: DirectLogin header request.headers.get(CIString("DirectLogin")) .map(h => parseDirectLoginHeader(h.head.value)) - .getOrElse(Map.empty) + .getOrElse { + // Fall back to old format: Authorization: DirectLogin token=xxx + request.headers.get(CIString("Authorization")) + .filter(_.head.value.contains("DirectLogin")) + .map(h => parseDirectLoginHeader(h.head.value)) + .getOrElse(Map.empty) + } } /** * Parse DirectLogin header value into parameter map - * Format: DirectLogin token="xxx", username="yyy" + * Matches Lift's parsing logic in directlogin.scala getAllParameters + * Supports formats: + * - DirectLogin token="xxx" + * - DirectLogin token=xxx + * - token="xxx", username="yyy" */ private def parseDirectLoginHeader(headerValue: String): Map[String, String] = { - val pattern = """(\w+)="([^"]*)"""".r - pattern.findAllMatchIn(headerValue).map { m => - m.group(1) -> m.group(2) + val directLoginPossibleParameters = List("consumer_key", "token", "username", "password") + + // Strip "DirectLogin" prefix and split by comma, then trim each part (matches Lift logic) + val cleanedParameterList = headerValue.stripPrefix("DirectLogin").split(",").map(_.trim).toList + + cleanedParameterList.flatMap { input => + if (input.contains("=")) { + val split = input.split("=", 2) + val paramName = split(0).trim + // Remove surrounding quotes if present + val paramValue = split(1).replaceAll("^\"|\"$", "").trim + if (directLoginPossibleParameters.contains(paramName) && paramValue.nonEmpty) + Some(paramName -> paramValue) + else + None + } else { + None + } }.toMap } diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 9dca59319..b20810748 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -63,11 +63,11 @@ object Http4s700 { EmptyBody, apiInfoJSON, List( - $UserNotLoggedIn, UnknownError, "no connector set" ), apiTagApi :: Nil, + Some(List(code.api.util.ApiRole.canGetRateLimits)), http4sPartialFunction = Some(root) )