Adding challengePurpose, challengeContextHash and

challengeContextStructure
This commit is contained in:
simonredfern 2026-01-20 07:05:13 +01:00
parent 3a77043bfc
commit 41a1506a66
4 changed files with 33 additions and 3 deletions

View File

@ -20,6 +20,10 @@ trait ChallengeProvider {
basketId: Option[String], // Note: basketId, consentId and transactionRequestId are exclusive here.
authenticationMethodId: Option[String],
challengeType: String,
// PSD2 Dynamic Linking fields
challengePurpose: Option[String] = None, // Human-readable description shown to user
challengeContextHash: Option[String] = None, // SHA-256 hash of critical transaction fields
challengeContextStructure: Option[String] = None // Comma-separated list of field names in hash
): Box[ChallengeTrait]
def getChallenge(challengeId: String): Box[ChallengeTrait]

View File

@ -28,8 +28,12 @@ object MappedChallengeProvider extends ChallengeProvider {
consentId: Option[String], // Note: consentId and transactionRequestId and basketId are exclusive here.
basketId: Option[String], // Note: consentId and transactionRequestId and basketId are exclusive here.
authenticationMethodId: Option[String],
challengeType: String,
): Box[ChallengeTrait] =
challengeType: String,
// PSD2 Dynamic Linking fields
challengePurpose: Option[String] = None,
challengeContextHash: Option[String] = None,
challengeContextStructure: Option[String] = None
): Box[ChallengeTrait] =
tryo (
MappedExpectedChallengeAnswer
.create
@ -44,6 +48,10 @@ object MappedChallengeProvider extends ChallengeProvider {
.ConsentId(consentId.getOrElse(""))
.BasketId(basketId.getOrElse(""))
.AuthenticationMethodId(expectedUserId)
// PSD2 Dynamic Linking
.ChallengePurpose(challengePurpose.getOrElse(""))
.ChallengeContextHash(challengeContextHash.getOrElse(""))
.ChallengeContextStructure(challengeContextStructure.getOrElse(""))
.saveMe()
)

View File

@ -29,6 +29,11 @@ class MappedExpectedChallengeAnswer extends ChallengeTrait with LongKeyedMapper[
override def defaultValue = 0
}
// PSD2 Dynamic Linking fields
object ChallengePurpose extends MappedString(this, 2000)
object ChallengeContextHash extends MappedString(this, 64)
object ChallengeContextStructure extends MappedString(this, 500)
override def challengeId: String = ChallengeId.get
override def challengeType: String = ChallengeType.get
override def transactionRequestId: String = TransactionRequestId.get
@ -42,6 +47,11 @@ class MappedExpectedChallengeAnswer extends ChallengeTrait with LongKeyedMapper[
override def scaStatus: Option[SCAStatus] = Option(StrongCustomerAuthenticationStatus.withName(ScaStatus.get))
override def authenticationMethodId: Option[String] = Option(AuthenticationMethodId.get)
override def attemptCounter: Int = AttemptCounter.get
// PSD2 Dynamic Linking
override def challengePurpose: Option[String] = Option(ChallengePurpose.get).filter(_.nonEmpty)
override def challengeContextHash: Option[String] = Option(ChallengeContextHash.get).filter(_.nonEmpty)
override def challengeContextStructure: Option[String] = Option(ChallengeContextStructure.get).filter(_.nonEmpty)
}
object MappedExpectedChallengeAnswer extends MappedExpectedChallengeAnswer with LongKeyedMetaMapper[MappedExpectedChallengeAnswer] {

View File

@ -647,8 +647,16 @@ trait ChallengeTrait {
def scaMethod: Option[SCA]
def scaStatus: Option[SCAStatus]
def authenticationMethodId: Option[String]
def attemptCounter: Int
// PSD2 Dynamic Linking support - these fields ensure the authentication is linked to the transaction details
// challenge_purpose: Human-readable description of what is being authorized (shown to user in SMS/email)
def challengePurpose: Option[String]
// challenge_context_hash: SHA-256 hash of critical transaction fields for tamper detection
def challengeContextHash: Option[String]
// challenge_context_structure: Comma-separated list of field names included in the hash
def challengeContextStructure: Option[String]
}