mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:27:05 +00:00
feature/(directlogin): add http4s support for DirectLogin authentication
- Add `validatorFutureWithParams` function to validate DirectLogin parameters extracted from CallContext without depending on S.request - Enhance `getUserFromDirectLoginHeaderFuture` to prefer DirectLogin parameters from CallContext (http4s path) and fall back to S.request (Lift path) - Improve `extractDirectLoginParams` to support both new format (DirectLogin header) and old format (Authorization: DirectLogin header) - Enhance `parseDirectLoginHeader` to match Lift's parsing logic with support for quoted and unquoted parameter values - Update Http4s700 API info to remove UserNotLoggedIn error and add canGetRateLimits role requirement - This enables DirectLogin authentication to work seamlessly in http4s context where S.request is unavailable
This commit is contained in:
parent
2d139b157e
commit
64b1ac3c9d
@ -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))
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user