mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 13:26:51 +00:00
Merge remote-tracking branch 'refs/remotes/UPSTREAM/develop' into develop
# Conflicts: # .github/workflows/build_container_develop_branch.yml # .github/workflows/build_container_non_develop_branch.yml # .github/workflows/build_contributer_container.yml # .github/workflows/build_package.yml
This commit is contained in:
commit
eac4cf4a7a
4
.github/workflows/build_pull_request.yml
vendored
4
.github/workflows/build_pull_request.yml
vendored
@ -27,9 +27,9 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
|
||||
6
.github/workflows/run_trivy.yml
vendored
6
.github/workflows/run_trivy.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- id: trivy-db
|
||||
name: Check trivy db sha
|
||||
env:
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
sha=$(gh api -H "${headers}" "${endpoint}" | jq --raw-output "${jqFilter}")
|
||||
echo "Trivy DB sha256:${sha}"
|
||||
echo "::set-output name=sha::${sha}"
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .trivy
|
||||
key: ${{ runner.os }}-trivy-db-${{ steps.trivy-db.outputs.sha }}
|
||||
@ -49,6 +49,6 @@ jobs:
|
||||
- name: Fix .trivy permissions
|
||||
run: sudo chown -R $(stat . -c %u:%g) .trivy
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
@ -0,0 +1,87 @@
|
||||
### TPP Certificate Verification
|
||||
|
||||
The signing and verification flow of certificates for TPP requests is a follows:
|
||||
|
||||
#### Generation of the Request by TPP
|
||||
|
||||
**Step 1: Calculating the Hash for the Digest Field**
|
||||
|
||||
* TPP creates the body of the request (e.g., JSON containing transaction or consent data).
|
||||
* TPP applies the SHA-256 algorithm to the body of the request to generate a unique hash.
|
||||
* The resulting hash is encoded in Base64 and placed in the request header under the `Digest` field.
|
||||
|
||||
**Step 2: Creating the Signing String**
|
||||
|
||||
* TPP prepares a signing string based on specific fields from the request header.
|
||||
* The signing string is a concatenation of the values of the signed fields in a specific format.
|
||||
* The order of the fields is critical and follows the specifications.
|
||||
|
||||
**Step 3: Signing the Signing String**
|
||||
|
||||
* TPP uses its RSA private key associated with its certificate to generate a digital signature.
|
||||
* The signature is applied to the signing string using the RSA-SHA256 algorithm.
|
||||
* The resulting digital signature is Base64-encoded and placed in the `Signature` field.
|
||||
|
||||
**Step 4: Adding the Certificate to the TPP-Signature-Certificate Field**
|
||||
|
||||
* TPP includes its public certificate in the request header under the `TPP-Signature-Certificate` field.
|
||||
* The certificate is issued by a trusted certification authority (CA) and contains the TPP's public key.
|
||||
|
||||
**Step 5: Sending the Request to OBP**
|
||||
|
||||
* TPP sends the complete request, including the `Digest`, `Signature`, and `TPP-Signature-Certificate` headers, to the OBP endpoint.
|
||||
|
||||
---
|
||||
|
||||
#### Verification of the Request by OBP
|
||||
|
||||
**Step 1: Validating the TPP Certificate**
|
||||
|
||||
* OBP verifies the TPP certificate included in the `TPP-Signature-Certificate` field.\
|
||||
Steps:
|
||||
|
||||
1. Verify the certificate's trust chain up to a trusted certification authority.
|
||||
2. Ensure the certificate is valid (not expired or revoked).
|
||||
3. Confirm the certificate is issued for the appropriate usage (e.g., digital signatures for Open Banking), based on the information from the National Bank registry (e.g., certificate SN and CA found in the `Signature` field).
|
||||
|
||||
* **Result**: If the certificate is valid, the TPP's identity is confirmed.
|
||||
|
||||
**Step 2: Verifying the Signature in the `Signature` Field**
|
||||
|
||||
* OBP extracts the fields included in the signing string based on the information in the `Signature` field.
|
||||
* OBP recreates the signing string in the same format used by the TPP.
|
||||
* OBP uses the public key from the certificate in the `TPP-Signature-Certificate` field to verify the signature in the `Signature` field.
|
||||
* **Result**: If the signature is valid, the authenticity of the request is confirmed.
|
||||
|
||||
**Step 3: Verifying the Hash in the `Digest` Field**
|
||||
|
||||
* OBP computes its own SHA-256 hash of the received request body.
|
||||
* The computed hash is compared with the value in the `Digest` header.
|
||||
* **Result**: If the two hashes match, the integrity of the request body is confirmed.
|
||||
|
||||
**Step 4: Authorizing the Request**
|
||||
|
||||
* If all verifications (integrity, signature, and certificate) pass, OBP processes the request.
|
||||
* Otherwise, the request is rejected with an error message (e.g., `401 Unauthorized` or `403 Forbidden`).
|
||||
|
||||
---
|
||||
|
||||
#### Additional Details
|
||||
|
||||
**How does OBP verify the signature in the `Signature` field?**
|
||||
|
||||
1. **Extracts the public key from the `TPP-Signature-Certificate`**:
|
||||
* OBP retrieves the certificate (`TPP-Signature-Certificate`) from the request header.
|
||||
* From this certificate, OBP extracts the TPP's public key.
|
||||
2. **Recreates the Signing String**:
|
||||
* OBP analyzes the `Signature` header to identify the signed fields (e.g., `Digest`, `X-Request-ID`, `Date`, etc.).
|
||||
* OBP recreates the signing string from the actual values of these fields in the specified order.
|
||||
3. **Decodes the Signature**:
|
||||
* OBP decodes the value in the `Signature.signature` field (the digital signature generated by TPP).
|
||||
* Decoding is done using the public key extracted from the `TPP-Signature-Certificate` and the algorithm specified in `Signature.algorithm` (e.g., RSA-SHA256).
|
||||
4. **Compares the Resulting Hash**:
|
||||
* During decoding, OBP obtains a hash calculated by TPP at the time of signing.
|
||||
* OBP compares this hash with the hash generated internally from the recreated signing string.
|
||||
5. **Possible Outcomes**:
|
||||
* If the hashes match, the signature is valid.
|
||||
* If the hashes do not match, the signature is invalid, and the request is rejected.
|
||||
@ -178,6 +178,12 @@ jwt.use.ssl=false
|
||||
# truststore.password.redis = truststore-password
|
||||
|
||||
|
||||
## Trust stores is a list of trusted CA certificates
|
||||
## Public certificate for the CA (used by clients and servers to validate signatures)
|
||||
# truststore.path.tpp_signature = path/to/ca.p12
|
||||
# truststore.password.tpp_signature = truststore-password
|
||||
|
||||
|
||||
## Enable writing API metrics (which APIs are called) to RDBMS
|
||||
write_metrics=true
|
||||
## Enable writing connector metrics (which methods are called)to RDBMS
|
||||
|
||||
@ -519,7 +519,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
||||
case Some(STATIC) => {
|
||||
val cacheValueFromRedis = Caching.getStaticResourceDocCache(cacheKey)
|
||||
|
||||
val dynamicDocs: Box[JValue] =
|
||||
val staticDocs: Box[JValue] =
|
||||
if (cacheValueFromRedis.isDefined) {
|
||||
Full(json.parse(cacheValueFromRedis.get))
|
||||
} else {
|
||||
@ -530,12 +530,12 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
||||
Full(resourceDocJsonJValue)
|
||||
}
|
||||
|
||||
Future(dynamicDocs.map(successJsonResponse(_)))
|
||||
Future(staticDocs.map(successJsonResponse(_)))
|
||||
}
|
||||
case _ => {
|
||||
val cacheValueFromRedis = Caching.getAllResourceDocCache(cacheKey)
|
||||
|
||||
val dynamicDocs: Box[JValue] =
|
||||
val bothStaticAndDyamicDocs: Box[JValue] =
|
||||
if (cacheValueFromRedis.isDefined) {
|
||||
Full(json.parse(cacheValueFromRedis.get))
|
||||
} else {
|
||||
@ -546,7 +546,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
||||
Full(resourceDocJsonJValue)
|
||||
}
|
||||
|
||||
Future(dynamicDocs.map(successJsonResponse(_)))
|
||||
Future(bothStaticAndDyamicDocs.map(successJsonResponse(_)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -715,13 +715,11 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
||||
swaggerJValue <- NewStyle.function.tryons(s"$UnknownError Can not convert internal swagger file.", 400, cc.callContext) {
|
||||
val cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey)
|
||||
|
||||
val dynamicDocs: JValue =
|
||||
if (cacheValueFromRedis.isDefined) {
|
||||
json.parse(cacheValueFromRedis.get)
|
||||
} else {
|
||||
convertResourceDocsToSwaggerJvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
|
||||
}
|
||||
dynamicDocs
|
||||
if (cacheValueFromRedis.isDefined) {
|
||||
json.parse(cacheValueFromRedis.get)
|
||||
} else {
|
||||
convertResourceDocsToSwaggerJvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
|
||||
}
|
||||
}
|
||||
} yield {
|
||||
(swaggerJValue, HttpCode.`200`(cc.callContext))
|
||||
|
||||
@ -61,7 +61,7 @@ object Redis extends MdcLoggable {
|
||||
|
||||
// Load the CA certificate
|
||||
val trustStore = KeyStore.getInstance(KeyStore.getDefaultType)
|
||||
val trustStorePassword = APIUtil.getPropsValue("keystore.password.redis")
|
||||
val trustStorePassword = APIUtil.getPropsValue("truststore.password.redis")
|
||||
.getOrElse(APIUtil.initPasswd).toCharArray
|
||||
val truststorePath = APIUtil.getPropsValue("truststore.path.redis").getOrElse("")
|
||||
val trustStoreStream = new FileInputStream(truststorePath)
|
||||
|
||||
@ -12,47 +12,48 @@
|
||||
"logo":"https://static.openbankproject.com/images/sandbox/bank_y.png",
|
||||
"website":"https://www.example.com"
|
||||
}],
|
||||
"users":[{
|
||||
"email":"robert.xuk.x@example.com",
|
||||
"user_name": "robert.xuk.x@example.com",
|
||||
"password":"5232e7",
|
||||
"display_name":"Robert XUk X"
|
||||
},{
|
||||
"email":"susan.xuk.x@example.com",
|
||||
"user_name": "susan.xuk.x@example.com",
|
||||
"password":"43ca4d",
|
||||
"display_name":"Susan XUk X"
|
||||
},{
|
||||
"email":"anil.xuk.x@example.com",
|
||||
"user_name": "anil.xuk.x@example.com",
|
||||
"password":"d8c716",
|
||||
"display_name":"Anil XUk X"
|
||||
},{
|
||||
"email":"ellie.xuk.x@example.com",
|
||||
"user_name": "ellie.xuk.x@example.com",
|
||||
"password":"6187b9",
|
||||
"display_name":"Ellie XUk X"
|
||||
},{
|
||||
"email":"robert.yuk.y@example.com",
|
||||
"user_name": "robert.yuk.y@example.com",
|
||||
"password":"e5046a",
|
||||
"display_name":"Robert YUk Y"
|
||||
},{
|
||||
"email":"susan.yuk.y@example.com",
|
||||
"user_name": "susan.yuk.y@example.com",
|
||||
"password":"5b38a6",
|
||||
"display_name":"Susan YUk Y"
|
||||
},{
|
||||
"email":"anil.yuk.y@example.com",
|
||||
"user_name": "anil.yuk.y@example.com",
|
||||
"password":"dcf03d",
|
||||
"display_name":"Anil YUk Y"
|
||||
},{
|
||||
"email":"ellie.yuk.y@example.com",
|
||||
"user_name": "ellie.yuk.y@example.com",
|
||||
"password":"4f9eaa",
|
||||
"display_name":"Ellie YUk Y"
|
||||
}],
|
||||
"users": [
|
||||
{
|
||||
"email": "robert.x.0.gh@example.com",
|
||||
"password": "V8%Ktssl(L",
|
||||
"user_name": "Robert.X.0.GH"
|
||||
},
|
||||
{
|
||||
"email": "susan.x.0.gh@example.com",
|
||||
"password": "naW9u3C%bh",
|
||||
"user_name": "Susan.X.0.GH"
|
||||
},
|
||||
{
|
||||
"email": "anil.x.0.gh@example.com",
|
||||
"password": "9W0RIrX-6f",
|
||||
"user_name": "Anil.X.0.GH"
|
||||
},
|
||||
{
|
||||
"email": "ellie.x.0.gh@example.com",
|
||||
"password": "rMf_OHM0dW",
|
||||
"user_name": "Ellie.X.0.GH"
|
||||
},
|
||||
{
|
||||
"email": "robert.y.9.gh@example.com",
|
||||
"password": "%1Z43kzt2L",
|
||||
"user_name": "Robert.Y.9.GH"
|
||||
},
|
||||
{
|
||||
"email": "susan.y.9.gh@example.com",
|
||||
"password": "oITehM!B2V",
|
||||
"user_name": "Susan.Y.9.GH"
|
||||
},
|
||||
{
|
||||
"email": "anil.y.9.gh@example.com",
|
||||
"password": "TuKaNO8oI-",
|
||||
"user_name": "Anil.Y.9.GH"
|
||||
},
|
||||
{
|
||||
"email": "ellie.y.9.gh@example.com",
|
||||
"password": "SkJDH+ds2_",
|
||||
"user_name": "Ellie.Y.9.GH"
|
||||
}
|
||||
],
|
||||
"accounts":[{
|
||||
"id":"05237266-b334-4704-a087-5b460a2ecf04",
|
||||
"bank":"psd201-bank-x--uk",
|
||||
|
||||
@ -12,39 +12,48 @@
|
||||
"logo":"https://static.openbankproject.com/images/sandbox/bank_y.png",
|
||||
"website":"https://www.example.com"
|
||||
}],
|
||||
"users":[{
|
||||
"email":"robert.x.0.gh@example.com",
|
||||
"password":"X!d1edcafd",
|
||||
"user_name":"Robert.X.0.GH"
|
||||
},{
|
||||
"email":"susan.x.0.gh@example.com",
|
||||
"password":"X!90e4e3e4",
|
||||
"user_name":"Susan.X.0.GH"
|
||||
},{
|
||||
"email":"anil.x.0.gh@example.com",
|
||||
"password":"X!eb06b005",
|
||||
"user_name":"Anil.X.0.GH"
|
||||
},{
|
||||
"email":"ellie.x.0.gh@example.com",
|
||||
"password":"X!5bc94405",
|
||||
"user_name":"Ellie.X.0.GH"
|
||||
},{
|
||||
"email":"robert.y.9.gh@example.com",
|
||||
"password":"X!039941de",
|
||||
"user_name":"Robert.Y.9.GH"
|
||||
},{
|
||||
"email":"susan.y.9.gh@example.com",
|
||||
"password":"X!bb4efa3d",
|
||||
"user_name":"Susan.Y.9.GH"
|
||||
},{
|
||||
"email":"anil.y.9.gh@example.com",
|
||||
"password":"X!098915cd",
|
||||
"user_name":"Anil.Y.9.GH"
|
||||
},{
|
||||
"email":"ellie.y.9.gh@example.com",
|
||||
"password":"X!6170b37b",
|
||||
"user_name":"Ellie.Y.9.GH"
|
||||
}],
|
||||
"users": [
|
||||
{
|
||||
"email": "robert.x.0.gh@example.com",
|
||||
"password": "V8%Ktssl(L",
|
||||
"user_name": "Robert.X.0.GH"
|
||||
},
|
||||
{
|
||||
"email": "susan.x.0.gh@example.com",
|
||||
"password": "naW9u3C%bh",
|
||||
"user_name": "Susan.X.0.GH"
|
||||
},
|
||||
{
|
||||
"email": "anil.x.0.gh@example.com",
|
||||
"password": "9W0RIrX-6f",
|
||||
"user_name": "Anil.X.0.GH"
|
||||
},
|
||||
{
|
||||
"email": "ellie.x.0.gh@example.com",
|
||||
"password": "rMf_OHM0dW",
|
||||
"user_name": "Ellie.X.0.GH"
|
||||
},
|
||||
{
|
||||
"email": "robert.y.9.gh@example.com",
|
||||
"password": "%1Z43kzt2L",
|
||||
"user_name": "Robert.Y.9.GH"
|
||||
},
|
||||
{
|
||||
"email": "susan.y.9.gh@example.com",
|
||||
"password": "oITehM!B2V",
|
||||
"user_name": "Susan.Y.9.GH"
|
||||
},
|
||||
{
|
||||
"email": "anil.y.9.gh@example.com",
|
||||
"password": "TuKaNO8oI-",
|
||||
"user_name": "Anil.Y.9.GH"
|
||||
},
|
||||
{
|
||||
"email": "ellie.y.9.gh@example.com",
|
||||
"password": "SkJDH+ds2_",
|
||||
"user_name": "Ellie.Y.9.GH"
|
||||
}
|
||||
],
|
||||
"accounts":[{
|
||||
"id":"f65e28a5-9abe-428f-85bb-6c3c38122adb",
|
||||
"bank":"obp-bank-x-gh",
|
||||
|
||||
@ -784,16 +784,24 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
* (?=.*\d) //should contain at least one digit
|
||||
* (?=.*[a-z]) //should contain at least one lower case
|
||||
* (?=.*[A-Z]) //should contain at least one upper case
|
||||
* (?=.*[!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]) //should contain at least one special character
|
||||
* ([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]{10,16}) //should contain 10 to 16 valid characters
|
||||
* (?=.*[!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]) //should contain at least one special character
|
||||
* ([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]{10,16}) //should contain 10 to 16 valid characters
|
||||
**/
|
||||
val regex =
|
||||
"""^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~])([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]{10,16})$""".r
|
||||
password match {
|
||||
case password if(password.length > 16 && password.length <= 512 && basicPasswordValidation(password) ==SILENCE_IS_GOLDEN) => true
|
||||
case regex(password) if(basicPasswordValidation(password) ==SILENCE_IS_GOLDEN) => true
|
||||
case _ => false
|
||||
"""^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]])([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]{10,16})$""".r
|
||||
|
||||
// first check `basicPasswordValidation`
|
||||
if (basicPasswordValidation(password) != SILENCE_IS_GOLDEN) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2nd: check the password length between 10 and 512
|
||||
if (password.length > 16 && password.length <= 512) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 3rd: check the regular expression
|
||||
regex.pattern.matcher(password).matches()
|
||||
}
|
||||
|
||||
/** only A-Z, a-z, 0-9,-,_,. =, & and max length <= 2048 */
|
||||
@ -847,12 +855,17 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
/** also support space now */
|
||||
def basicPasswordValidation(value:String): String ={
|
||||
val valueLength = value.length
|
||||
val regex = """^([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~ ]+)$""".r
|
||||
value match {
|
||||
case regex(e) if(valueLength <= 512) => SILENCE_IS_GOLDEN
|
||||
case regex(e) if(valueLength > 512) => ErrorMessages.InvalidValueLength
|
||||
case _ => ErrorMessages.InvalidValueCharacters
|
||||
val regex = """^([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~ \[\]]+)$""".r
|
||||
|
||||
if (!regex.pattern.matcher(value).matches()) {
|
||||
return ErrorMessages.InvalidValueCharacters
|
||||
}
|
||||
|
||||
if (valueLength > 512) {
|
||||
return ErrorMessages.InvalidValueLength
|
||||
}
|
||||
|
||||
SILENCE_IS_GOLDEN
|
||||
}
|
||||
|
||||
/** only A-Z, a-z, 0-9, -, _, ., @, and max length <= 512 */
|
||||
|
||||
@ -130,7 +130,12 @@ object BerlinGroupSigning {
|
||||
val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders)
|
||||
val signature = parseSignatureHeader(signatureHeaderValue).getOrElse("signature", "NONE")
|
||||
val isVerified = verifySignature(signingString, signature, certificatePem)
|
||||
if (isVerified) forwardResult else (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2)
|
||||
val isValidated = CertificateVerifier.validateCertificate(certificatePem)
|
||||
(isVerified, isValidated) match {
|
||||
case (true, true) => forwardResult
|
||||
case (true, false) => (Failure(ErrorMessages.X509PublicKeyCannotBeValidated), forwardResult._2)
|
||||
case (false, _) => (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2)
|
||||
}
|
||||
case Failure(msg, t, c) => (Failure(msg, t, c), forwardResult._2) // PEM certificate is not valid
|
||||
case _ => (Failure(ErrorMessages.X509GeneralError), forwardResult._2) // PEM certificate cannot be validated
|
||||
}
|
||||
|
||||
153
obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
Normal file
153
obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
Normal file
@ -0,0 +1,153 @@
|
||||
package code.api.util
|
||||
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import java.io.{ByteArrayInputStream, FileInputStream}
|
||||
import java.security.KeyStore
|
||||
import java.security.cert._
|
||||
import java.util.{Base64, Collections}
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import scala.io.Source
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
object CertificateVerifier extends MdcLoggable {
|
||||
|
||||
/**
|
||||
* Loads a trust store (`.p12` file) from a configured path.
|
||||
*
|
||||
* This function:
|
||||
* - Reads the trust store password from the application properties (`truststore.path.tpp_signature`).
|
||||
* - Uses Java's `KeyStore` class to load the certificates.
|
||||
*
|
||||
* @return An `Option[KeyStore]` containing the loaded trust store, or `None` if loading fails.
|
||||
*/
|
||||
private def loadTrustStore(): Option[KeyStore] = {
|
||||
val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature")
|
||||
.or(APIUtil.getPropsValue("truststore.path")).getOrElse("")
|
||||
val trustStorePassword = APIUtil.getPropsValue("truststore.password.tpp_signature", "").toCharArray
|
||||
|
||||
Try {
|
||||
val trustStore = KeyStore.getInstance("PKCS12")
|
||||
val trustStoreInputStream = new FileInputStream(trustStorePath)
|
||||
try {
|
||||
trustStore.load(trustStoreInputStream, trustStorePassword)
|
||||
} finally {
|
||||
trustStoreInputStream.close()
|
||||
}
|
||||
trustStore
|
||||
} match {
|
||||
case Success(store) =>
|
||||
logger.info(s"Loaded trust store from: $trustStorePath")
|
||||
Some(store)
|
||||
case Failure(e) =>
|
||||
logger.info(s"Failed to load trust store: ${e.getMessage}")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an X.509 certificate against the loaded trust store.
|
||||
*
|
||||
* This function:
|
||||
* - Parses the PEM certificate into an `X509Certificate` using `parsePemToX509Certificate`.
|
||||
* - Loads the trust store using `loadTrustStore()`.
|
||||
* - Extracts trusted root CAs from the trust store.
|
||||
* - Creates PKIX validation parameters and disables revocation checking.
|
||||
* - Validates the certificate using Java's `CertPathValidator`.
|
||||
*
|
||||
* @param pemCertificate The X.509 certificate in PEM format.
|
||||
* @return `true` if the certificate is valid and trusted, otherwise `false`.
|
||||
*/
|
||||
def validateCertificate(pemCertificate: String): Boolean = {
|
||||
Try {
|
||||
val certificate = parsePemToX509Certificate(pemCertificate)
|
||||
|
||||
// Load trust store
|
||||
val trustStore = loadTrustStore()
|
||||
.getOrElse(throw new Exception("Trust store could not be loaded."))
|
||||
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
|
||||
trustManagerFactory.init(trustStore)
|
||||
|
||||
// Get trusted CAs from the trust store
|
||||
val trustAnchors = trustStore.aliases().asScala
|
||||
.filter(trustStore.isCertificateEntry)
|
||||
.map(alias => trustStore.getCertificate(alias).asInstanceOf[X509Certificate])
|
||||
.map(cert => new TrustAnchor(cert, null))
|
||||
.toSet
|
||||
.asJava // Convert Scala Set to Java Set
|
||||
|
||||
if (trustAnchors.isEmpty) throw new Exception("No trusted certificates found in trust store.")
|
||||
|
||||
// Set up PKIX parameters for validation
|
||||
val pkixParams = new PKIXParameters(trustAnchors)
|
||||
pkixParams.setRevocationEnabled(false) // Disable CRL checks
|
||||
|
||||
// Validate certificate chain
|
||||
val certPath = CertificateFactory.getInstance("X.509").generateCertPath(Collections.singletonList(certificate))
|
||||
val validator = CertPathValidator.getInstance("PKIX")
|
||||
validator.validate(certPath, pkixParams)
|
||||
|
||||
logger.info("Certificate is valid and trusted.")
|
||||
true
|
||||
} match {
|
||||
case Success(_) => true
|
||||
case Failure(e: CertPathValidatorException) =>
|
||||
logger.info(s"Certificate validation failed: ${e.getMessage}")
|
||||
false
|
||||
case Failure(e) =>
|
||||
logger.info(s"Error: ${e.getMessage}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PEM certificate (Base64-encoded) into an `X509Certificate` object.
|
||||
*
|
||||
* This function:
|
||||
* - Removes the PEM header and footer (`-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----`).
|
||||
* - Decodes the Base64-encoded certificate data.
|
||||
* - Generates and returns an `X509Certificate` object.
|
||||
*
|
||||
* @param pem The X.509 certificate in PEM format.
|
||||
* @return The parsed `X509Certificate` object.
|
||||
*/
|
||||
private def parsePemToX509Certificate(pem: String): X509Certificate = {
|
||||
val cleanedPem = pem.replaceAll("-----BEGIN CERTIFICATE-----", "")
|
||||
.replaceAll("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s", "")
|
||||
|
||||
val decoded = Base64.getDecoder.decode(cleanedPem)
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
certFactory.generateCertificate(new ByteArrayInputStream(decoded)).asInstanceOf[X509Certificate]
|
||||
}
|
||||
|
||||
def loadPemCertificateFromFile(filePath: String): Option[String] = {
|
||||
Try {
|
||||
val source = Source.fromFile(filePath)
|
||||
try source.getLines().mkString("\n") // Read entire file into a single string
|
||||
finally source.close()
|
||||
} match {
|
||||
case Success(pem) => Some(pem)
|
||||
case Failure(exception) =>
|
||||
logger.error(s"Failed to load PEM certificate from file: ${exception.getMessage}")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
// change the following path if using this function to test on your localhost
|
||||
val certificatePath = "/path/to/certificate.pem"
|
||||
val pemCertificate = loadPemCertificateFromFile(certificatePath)
|
||||
|
||||
pemCertificate.foreach { pem =>
|
||||
val isValid = validateCertificate(pem)
|
||||
logger.info(s"Certificate verification result: $isValid")
|
||||
}
|
||||
|
||||
loadTrustStore().foreach { trustStore =>
|
||||
logger.info(s"Trust Store contains ${trustStore.size()} certificates.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -279,6 +279,7 @@ object ErrorMessages {
|
||||
val X509ThereAreNoPsd2Roles = "OBP-20308: PEM Encoded Certificate does not contain PSD2 roles."
|
||||
val X509CannotGetPublicKey = "OBP-20309: Public key cannot be found in the PEM Encoded Certificate."
|
||||
val X509PublicKeyCannotVerify = "OBP-20310: Certificate's public key cannot be used to verify signed request."
|
||||
val X509PublicKeyCannotBeValidated = "OBP-20312: Certificate's public key cannot be validated."
|
||||
val X509RequestIsNotSigned = "OBP-20311: The Request is not signed."
|
||||
|
||||
// OpenID Connect
|
||||
|
||||
@ -72,7 +72,9 @@ trait OBPDataImport extends MdcLoggable {
|
||||
protected def dataOrFirstFailure[T](boxes : List[Box[T]]) = {
|
||||
val firstFailure = boxes.collectFirst{case f: Failure => f}
|
||||
firstFailure match {
|
||||
case Some(f) => f
|
||||
case Some(f) =>
|
||||
logger.debug(f)
|
||||
f
|
||||
case None => Full(boxes.flatten) //no failures, so we can return the results
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,89 +1,302 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2019, TESOBE GmbH.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH.
|
||||
Osloer Strasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
|
||||
*/
|
||||
* Open Bank Project - API
|
||||
* Copyright (C) 2011-2019, TESOBE GmbH.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Email: contact@tesobe.com
|
||||
* TESOBE GmbH.
|
||||
* Osloer Strasse 16/17
|
||||
* Berlin 13359, Germany
|
||||
*
|
||||
* This product includes software developed at
|
||||
* TESOBE (http://www.tesobe.com/)
|
||||
*/
|
||||
package code.snippet
|
||||
|
||||
import code.accountholders.AccountHolders
|
||||
import code.api.RequestHeader
|
||||
import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{GetConsentResponseJson, createGetConsentResponseJson}
|
||||
import code.api.util.{ConsentJWT, CustomJsonFormats, JwtUtil}
|
||||
import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessAccountsJson, ConsentAccessJson, GetConsentResponseJson, createGetConsentResponseJson}
|
||||
import code.api.util.ErrorMessages.ConsentNotFound
|
||||
import code.api.util._
|
||||
import code.api.v3_1_0.APIMethods310
|
||||
import code.api.v5_0_0.APIMethods500
|
||||
import code.api.v5_1_0.APIMethods510
|
||||
import code.consent.{ConsentStatus, Consents, MappedConsent}
|
||||
import code.consumer.Consumers
|
||||
import code.model.dataAccess.AuthUser
|
||||
import code.model.dataAccess.{AuthUser, BankAccountRouting}
|
||||
import code.util.Helper.{MdcLoggable, ObpS}
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
import com.openbankproject.commons.model.BankIdAccountId
|
||||
import net.liftweb.common.{Box, Failure, Full}
|
||||
import net.liftweb.http.js.JsCmds
|
||||
import net.liftweb.http.rest.RestHelper
|
||||
import net.liftweb.http.{RequestVar, S, SHtml, SessionVar}
|
||||
import net.liftweb.http.{S, SHtml, SessionVar}
|
||||
import net.liftweb.json.{Formats, parse}
|
||||
import net.liftweb.mapper.By
|
||||
import net.liftweb.util.CssSel
|
||||
import net.liftweb.util.Helpers._
|
||||
|
||||
import scala.collection.immutable
|
||||
import scala.concurrent.Future
|
||||
import scala.xml.NodeSeq
|
||||
|
||||
/**
|
||||
* This class handles Berlin Group consent requests.
|
||||
* It provides functionality to confirm or deny consent requests,
|
||||
* and manages the consent process for accessing account data.
|
||||
*/
|
||||
class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 with APIMethods500 with APIMethods310 {
|
||||
// Custom JSON formats for serialization/deserialization
|
||||
protected implicit override def formats: Formats = CustomJsonFormats.formats
|
||||
|
||||
private object otpValue extends SessionVar("123")
|
||||
private object redirectUriValue extends SessionVar("")
|
||||
// Session variables to store OTP, redirect URI, and other consent-related data
|
||||
private object otpValue extends SessionVar("123") // Stores the OTP value for SCA (Strong Customer Authentication)
|
||||
private object redirectUriValue extends SessionVar("") // Stores the redirect URI for post-consent actions
|
||||
private object updateConsentPayloadValue extends SessionVar(false) // Flag to indicate if consent payload needs updating
|
||||
private object userIsOwnerOfAccountsValue extends SessionVar(true) // Flag to check if the user owns the accounts
|
||||
|
||||
// Session variables to store selected IBANs for accounts, balances, and transactions
|
||||
private object selectedAccountsIbansValue extends SessionVar[Set[String]](Set()) {
|
||||
override def set(value: Set[String]): Set[String] = {
|
||||
logger.debug(s"selectedAccountsIbansValue changed to: ${value.mkString(", ")}")
|
||||
super.set(value)
|
||||
}
|
||||
}
|
||||
private object accessAccountsDefinedVar extends SessionVar(true)
|
||||
private object accessBalancesDefinedVar extends SessionVar(true)
|
||||
private object accessTransactionsDefinedVar extends SessionVar(true)
|
||||
/**
|
||||
* Creates a ConsentAccessJson object from lists of IBANs for accounts, balances, and transactions.
|
||||
*
|
||||
* @param accounts List of IBANs for accounts.
|
||||
* @param balances List of IBANs for balances.
|
||||
* @param transactions List of IBANs for transactions.
|
||||
* @return ConsentAccessJson object.
|
||||
*/
|
||||
def createConsentAccessJson(accounts: List[String], balances: List[String], transactions: List[String]): ConsentAccessJson = {
|
||||
val accountsList = accounts.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None))
|
||||
val balancesList = balances.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None))
|
||||
val transactionsList = transactions.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None))
|
||||
|
||||
ConsentAccessJson(
|
||||
accounts = Some(accountsList), // Populate accounts
|
||||
balances = if (balancesList.nonEmpty) Some(balancesList) else None, // Populate balances
|
||||
transactions = if (transactionsList.nonEmpty) Some(transactionsList) else None // Populate transactions
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the consent with new IBANs for accounts, balances, and transactions.
|
||||
*
|
||||
* @param consentId The ID of the consent to update.
|
||||
* @param ibans List of IBANs for accounts.
|
||||
* @return Future[MappedConsent] representing the updated consent.
|
||||
*/
|
||||
private def updateConsent(consentId: String, ibans: List[String], canReadBalances: Boolean, canReadTransactions: Boolean): Future[MappedConsent] = {
|
||||
for {
|
||||
// Fetch the consent by ID
|
||||
consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map {
|
||||
APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 404)
|
||||
}
|
||||
// Update the consent JWT with new access details
|
||||
consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT(
|
||||
createConsentAccessJson(
|
||||
ibans,
|
||||
if(canReadBalances) ibans else List(),
|
||||
if(canReadTransactions) ibans else List()
|
||||
),
|
||||
consent,
|
||||
None
|
||||
) map {
|
||||
i => APIUtil.connectorEmptyResponse(i, None)
|
||||
}
|
||||
// Save the updated consent
|
||||
updatedConsent <- Future(Consents.consentProvider.vend.setJsonWebToken(consent.consentId, consentJWT)) map {
|
||||
i => APIUtil.connectorEmptyResponse(i, None)
|
||||
}
|
||||
} yield {
|
||||
updatedConsent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the Berlin Group consent confirmation form.
|
||||
*
|
||||
* @return CssSel for rendering the form.
|
||||
*/
|
||||
def confirmBerlinGroupConsentRequest: CssSel = {
|
||||
callGetConsentByConsentId() match {
|
||||
case Full(consent) =>
|
||||
// Set OTP and redirect URI from the consent
|
||||
otpValue.set(consent.challenge)
|
||||
val json: GetConsentResponseJson = createGetConsentResponseJson(consent)
|
||||
val consumer = Consumers.consumers.vend.getConsumerByConsumerId(consent.consumerId)
|
||||
val consentJwt: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken).map(parse(_)
|
||||
.extract[ConsentJWT])
|
||||
val tppRedirectUri: immutable.Seq[String] = consentJwt.map{ h =>
|
||||
val tppRedirectUri: immutable.Seq[String] = consentJwt.map { h =>
|
||||
h.request_headers.filter(h => h.name == RequestHeader.`TPP-Redirect-URL`)
|
||||
}.getOrElse(Nil).map((_.values.mkString("")))
|
||||
val consumerRedirectUri: Option[String] = consumer.map(_.redirectURL.get).toOption
|
||||
val uri: String = tppRedirectUri.headOption.orElse(consumerRedirectUri).getOrElse("https://not.defined.com")
|
||||
redirectUriValue.set(uri)
|
||||
|
||||
// Get all accounts held by the current user
|
||||
val userAccounts: Set[BankIdAccountId] =
|
||||
AccountHolders.accountHolders.vend.getAccountsHeldByUser(AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null)).toSet
|
||||
val userIbans: Set[String] = userAccounts.flatMap { acc =>
|
||||
BankAccountRouting.find(
|
||||
By(BankAccountRouting.BankId, acc.bankId.value),
|
||||
By(BankAccountRouting.AccountId, acc.accountId.value),
|
||||
By(BankAccountRouting.AccountRoutingScheme, "IBAN")
|
||||
).map(_.AccountRoutingAddress.get)
|
||||
}
|
||||
// Select all IBANs
|
||||
selectedAccountsIbansValue.set(userIbans)
|
||||
|
||||
// Determine which IBANs the user can access for accounts, balances, and transactions
|
||||
val canReadAccountsIbans: List[String] = json.access.accounts match {
|
||||
case Some(accounts) if accounts.isEmpty => // Access is requested
|
||||
updateConsentPayloadValue.set(true)
|
||||
accessAccountsDefinedVar.set(true)
|
||||
userIbans.toList
|
||||
case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs
|
||||
accessAccountsDefinedVar.set(true)
|
||||
accounts.flatMap(_.iban)
|
||||
case Some(accounts) => // Logged in user is not an owner of IBAN/IBANs
|
||||
userIsOwnerOfAccountsValue.set(false)
|
||||
accessAccountsDefinedVar.set(true)
|
||||
accounts.flatMap(_.iban)
|
||||
case None => // Access is not requested
|
||||
accessAccountsDefinedVar.set(false)
|
||||
List()
|
||||
}
|
||||
val canReadBalancesIbans: List[String] = json.access.balances match {
|
||||
case Some(balances) if balances.isEmpty => // Access is requested
|
||||
updateConsentPayloadValue.set(true)
|
||||
accessBalancesDefinedVar.set(true)
|
||||
userIbans.toList
|
||||
case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs
|
||||
accessBalancesDefinedVar.set(true)
|
||||
balances.flatMap(_.iban)
|
||||
case Some(balances) => // Logged in user is not an owner of IBAN/IBANs
|
||||
userIsOwnerOfAccountsValue.set(false)
|
||||
accessBalancesDefinedVar.set(true)
|
||||
balances.flatMap(_.iban)
|
||||
case None => // Access is not requested
|
||||
accessBalancesDefinedVar.set(false)
|
||||
List()
|
||||
}
|
||||
val canReadTransactionsIbans: List[String] = json.access.transactions match {
|
||||
case Some(transactions) if transactions.isEmpty => // Access is requested
|
||||
updateConsentPayloadValue.set(true)
|
||||
accessTransactionsDefinedVar.set(true)
|
||||
userIbans.toList
|
||||
case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs
|
||||
accessTransactionsDefinedVar.set(true)
|
||||
transactions.flatMap(_.iban)
|
||||
case Some(transactions) => // Logged in user is not an owner of IBAN/IBANs
|
||||
userIsOwnerOfAccountsValue.set(false)
|
||||
accessTransactionsDefinedVar.set(true)
|
||||
transactions.flatMap(_.iban)
|
||||
case None => // Access is not requested
|
||||
accessTransactionsDefinedVar.set(false)
|
||||
List()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates toggle switches for IBAN lists.
|
||||
*
|
||||
* @param scope The scope of the IBANs (e.g., "canReadAccountsIbans").
|
||||
* @param ibans List of IBANs to display.
|
||||
* @param selectedList Set of currently selected IBANs.
|
||||
* @param sessionVar Session variable to update when toggling.
|
||||
* @return Sequence of NodeSeq representing the toggle switches.
|
||||
*/
|
||||
def generateCheckboxes(scope: String, ibans: List[String], selectedList: Set[String], sessionVar: SessionVar[Set[String]]): immutable.Seq[NodeSeq] = {
|
||||
ibans.map { iban =>
|
||||
if (updateConsentPayloadValue.is) {
|
||||
// Show toggle switch when updateConsentPayloadValue is true
|
||||
<div class="toggle-container">
|
||||
<label class="switch">
|
||||
{SHtml.ajaxCheckbox(selectedList.contains(iban), checked => {
|
||||
if (checked) {
|
||||
sessionVar.set(selectedList + iban) // Add to selected
|
||||
} else {
|
||||
sessionVar.set(selectedList - iban) // Remove from selected
|
||||
}
|
||||
JsCmds.Noop // Prevents page reload
|
||||
}, "id" -> (iban + scope), "class" -> "toggle-input")}<span class="slider round"></span>
|
||||
</label>
|
||||
<span style="all: unset;" class="toggle-label">
|
||||
{iban}
|
||||
</span>
|
||||
</div>
|
||||
} else {
|
||||
// Show only the IBAN text when updateConsentPayloadValue is false
|
||||
<span style="all: unset;" class="toggle-label">
|
||||
{iban}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form text and user details
|
||||
val currentUser = AuthUser.currentUser
|
||||
val firstName = currentUser.map(_.firstName.get).getOrElse("")
|
||||
val lastName = currentUser.map(_.lastName.get).getOrElse("")
|
||||
val consumerName = consumer.map(_.name.get).getOrElse("")
|
||||
val formText =
|
||||
s"""I, ${AuthUser.currentUser.map(_.firstName.get).getOrElse("")} ${AuthUser.currentUser.map(_.lastName.get).getOrElse("")}, consent to the service provider ${consumer.map(_.name.get).getOrElse("")} making actions on my behalf.
|
||||
|
|
||||
|This consent must respects the following actions:
|
||||
|
|
||||
| 1) Can read accounts: ${json.access.accounts.getOrElse(Nil).flatMap(_.iban).mkString(", ")}
|
||||
| 2) Can read balances: ${json.access.balances.getOrElse(Nil).flatMap(_.iban).mkString(", ")}
|
||||
| 3) Can read transactions: ${json.access.transactions.getOrElse(Nil).flatMap(_.iban).mkString(", ")}
|
||||
|
|
||||
|This consent will end on date ${json.validUntil}.
|
||||
|
|
||||
|I understand that I can revoke this consent at any time.
|
||||
|""".stripMargin
|
||||
s"""I, $firstName $lastName, consent to the service provider <strong>$consumerName</strong> making the following actions on my behalf:
|
||||
|""".stripMargin
|
||||
|
||||
// Converting formText into a NodeSeq for raw HTML
|
||||
val formTextHtml: NodeSeq = scala.xml.XML.loadString("<div>" + formText + "</div>")
|
||||
|
||||
"#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" &
|
||||
"#confirm-bg-consent-request-form-text *" #> s"""$formText""" &
|
||||
// Form rendering
|
||||
"#confirm-bg-consent-request-form-title *" #> "Please confirm or deny the following consent request:" &
|
||||
"#confirm-bg-consent-request-form-text *" #> (
|
||||
<div>
|
||||
<p>
|
||||
{formTextHtml}
|
||||
</p>
|
||||
<div>
|
||||
<p><strong>Allowed actions:</strong></p>
|
||||
<p style="padding-left: 20px">Read account details</p>
|
||||
<p style={if (accessBalancesDefinedVar.is) "padding-left: 20px;" else "padding-left: 20px; display: none;"}>Read account balances</p>
|
||||
<p style={if (accessTransactionsDefinedVar.is) "padding-left: 20px;" else "padding-left: 20px; display: none;"}>Read transactions</p>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>Accounts</strong>:</p>
|
||||
<div style="padding-left: 20px">
|
||||
{generateCheckboxes("canReadAccountsIbans", userIbans.toList, selectedAccountsIbansValue.is, selectedAccountsIbansValue)}
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<p>This consent will end on date {json.validUntil}.</p>
|
||||
<p>I understand that I can revoke this consent at any time.</p>
|
||||
</div>
|
||||
) & {
|
||||
if (userIsOwnerOfAccountsValue) {
|
||||
"#confirm-bg-consent-request-confirm-submit-button" #> SHtml.onSubmitUnit(confirmConsentRequestProcess) &
|
||||
"#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess)
|
||||
"#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess)
|
||||
} else {
|
||||
S.error(s"User $firstName $lastName is not owner of listed accounts")
|
||||
"#confirm-bg-consent-request-confirm-submit-button" #> "" &
|
||||
"#confirm-bg-consent-request-deny-submit-button" #> ""
|
||||
}}
|
||||
|
||||
case everythingElse =>
|
||||
S.error(everythingElse.toString)
|
||||
"#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" &
|
||||
@ -91,6 +304,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a consent by its ID.
|
||||
*
|
||||
* @return Box[MappedConsent] containing the consent if found.
|
||||
*/
|
||||
private def callGetConsentByConsentId(): Box[MappedConsent] = {
|
||||
val requestParam = List(
|
||||
ObpS.param("CONSENT_ID"),
|
||||
@ -103,12 +321,31 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the confirmation of a consent request.
|
||||
*/
|
||||
private def confirmConsentRequestProcess() = {
|
||||
val consentId = ObpS.param("CONSENT_ID") openOr ("")
|
||||
S.redirectTo(
|
||||
s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}"
|
||||
)
|
||||
if (selectedAccountsIbansValue.is.isEmpty) {
|
||||
S.error(s"Please select at least 1 account")
|
||||
} else {
|
||||
val consentId = ObpS.param("CONSENT_ID") openOr ("")
|
||||
if (updateConsentPayloadValue.is) {
|
||||
updateConsent(
|
||||
consentId,
|
||||
selectedAccountsIbansValue.is.toList,
|
||||
accessBalancesDefinedVar.is,
|
||||
accessTransactionsDefinedVar.is
|
||||
)
|
||||
}
|
||||
S.redirectTo(
|
||||
s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the denial of a consent request.
|
||||
*/
|
||||
private def denyConsentRequestProcess() = {
|
||||
val consentId = ObpS.param("CONSENT_ID") openOr ("")
|
||||
Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected)
|
||||
@ -116,6 +353,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
|
||||
s"$redirectUriValue?CONSENT_ID=${consentId}"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the confirmation of a consent request with SCA (Strong Customer Authentication).
|
||||
*/
|
||||
private def confirmConsentRequestProcessSca() = {
|
||||
val consentId = ObpS.param("CONSENT_ID") openOr ("")
|
||||
Consents.consentProvider.vend.getConsentByConsentId(consentId) match {
|
||||
@ -129,7 +370,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders the SCA confirmation form for Berlin Group consent.
|
||||
*
|
||||
* @return CssSel for rendering the form.
|
||||
*/
|
||||
def confirmBgConsentRequest: CssSel = {
|
||||
"#otp-value" #> SHtml.text(otpValue, otpValue(_)) &
|
||||
"type=submit" #> SHtml.onSubmitUnit(confirmConsentRequestProcessSca)
|
||||
|
||||
@ -32,7 +32,7 @@ Berlin 13359, Germany
|
||||
<div class="form-group">
|
||||
<h3 id="confirm-bg-consent-request-form-title">Please check the Berlin Group Consent Request: </h3>
|
||||
<div id="confirm-bg-consent-request-form-text-div">
|
||||
<pre id="confirm-bg-consent-request-form-text"></pre>
|
||||
<div id="confirm-bg-consent-request-form-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post">
|
||||
|
||||
@ -37,6 +37,7 @@ import code.api.util._
|
||||
import code.setup.PropsReset
|
||||
import code.util.Helper.SILENCE_IS_GOLDEN
|
||||
import com.openbankproject.commons.model.UserAuthContextCommons
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import net.liftweb.common.{Box, Empty, Full}
|
||||
import net.liftweb.http.provider.HTTPParam
|
||||
import net.liftweb.json.{JValue, parse}
|
||||
@ -838,6 +839,65 @@ class APIUtilTest extends FeatureSpec with Matchers with GivenWhenThen with Prop
|
||||
actualValue6 contains (InvalidValueLength) shouldBe (true)
|
||||
|
||||
}
|
||||
|
||||
scenario(s"Test the ${nameOf(APIUtil.basicPasswordValidation _)} method") {
|
||||
val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]"
|
||||
|
||||
basicPasswordValidation(firefoxStrongPasswordProposal) shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN
|
||||
basicPasswordValidation("Abc!123 xyz") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN
|
||||
basicPasswordValidation("SuperStrong#123") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN
|
||||
basicPasswordValidation("Hello World!") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN
|
||||
basicPasswordValidation(" ") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN allow space so far
|
||||
|
||||
basicPasswordValidation("short💥") shouldBe (InvalidValueCharacters) // ❌ ErrorMessages.InvalidValueCharacters
|
||||
basicPasswordValidation("a" * 513) shouldBe (InvalidValueLength) // ❌ ErrorMessages.InvalidValueLength
|
||||
|
||||
}
|
||||
|
||||
scenario(s"Test the ${nameOf(APIUtil.fullPasswordValidation _)} method") {
|
||||
val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]"
|
||||
|
||||
fullPasswordValidation(firefoxStrongPasswordProposal) // ✅ true
|
||||
fullPasswordValidation("Abc!123xyz") // ✅ true
|
||||
fullPasswordValidation("SuperStrong#123") // ✅ true
|
||||
fullPasswordValidation("Abcdefg!1") // ✅ true
|
||||
fullPasswordValidation("short1!") // ❌ false(too short)
|
||||
fullPasswordValidation("alllowercase123!") // ❌ false(no capital letter)
|
||||
fullPasswordValidation("ALLUPPERCASE123!") // ❌ false(no smaller case letter)
|
||||
fullPasswordValidation("NoSpecialChar123") // ❌ false(not special character)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
feature(s"test ${nameOf(APIUtil.basicPasswordValidation _)} and ${nameOf(APIUtil.fullPasswordValidation _)}") {
|
||||
|
||||
scenario(s"Test the ${nameOf(APIUtil.basicPasswordValidation _)} method") {
|
||||
val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]"
|
||||
|
||||
basicPasswordValidation(firefoxStrongPasswordProposal) shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN
|
||||
basicPasswordValidation("Abc!123 xyz") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN
|
||||
basicPasswordValidation("SuperStrong#123") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN
|
||||
basicPasswordValidation("Hello World!") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN
|
||||
basicPasswordValidation(" ") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN allow space so far
|
||||
|
||||
basicPasswordValidation("short💥") shouldBe (InvalidValueCharacters) // ❌ ErrorMessages.InvalidValueCharacters
|
||||
basicPasswordValidation("a" * 513) shouldBe (InvalidValueLength) // ❌ ErrorMessages.InvalidValueLength
|
||||
|
||||
}
|
||||
|
||||
scenario(s"Test the ${nameOf(APIUtil.fullPasswordValidation _)} method") {
|
||||
val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]"
|
||||
|
||||
fullPasswordValidation(firefoxStrongPasswordProposal) // ✅ true
|
||||
fullPasswordValidation("Abc!123xyz") // ✅ true
|
||||
fullPasswordValidation("SuperStrong#123") // ✅ true
|
||||
fullPasswordValidation("Abcdefg!1") // ✅ true
|
||||
fullPasswordValidation("short1!") // ❌ false(too short)
|
||||
fullPasswordValidation("alllowercase123!") // ❌ false(no capital letter)
|
||||
fullPasswordValidation("ALLUPPERCASE123!") // ❌ false(no smaller case letter)
|
||||
fullPasswordValidation("NoSpecialChar123") // ❌ false(not special character)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user