diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 97b266b36f..1c123dcfbb 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -280,6 +280,10 @@ db.url=jdbc:h2:./lift_proto.db;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE ## (this needs to be a URL) hostname=http://127.0.0.1:8080 +# Used as the base URL for password reset and email validation links sent via email. +# Set this to your frontend/portal URL so that emails contain the correct link. +portal_external_url=http://localhost:5174 + ## This is only useful for running the api locally via RunWebApp ## If you use it, make sure this matches your hostname port! ## If you want to change the port when running via the command line, use "mvn -Djetty.port=8080 jetty:run" instead @@ -813,7 +817,13 @@ autocomplete_at_login_form_enabled=false # This involves this OBP-API sending an email to the newly registered email provided by the User and the User clicking on a link in that email # which results in a field being changed in the database. # To BYPASS this security features (for local development only), set this property to true to skip the email address validation. -#authUser.skipEmailValidation=false +authUser.skipEmailValidation=false + +# Expiry time in minutes for email validation JWT tokens (default: 1440 = 24 hours) +email_validation_token_expiry_minutes=1440 + +# Expiry time in minutes for password reset JWT tokens (default: 120 = 2 hours) +password_reset_token_expiry_minutes=120 # control the create and access to public views. # allow_public_views=false diff --git a/obp-api/src/main/resources/props/test.default.props.template b/obp-api/src/main/resources/props/test.default.props.template index c72d0ec8bc..7bdba328af 100644 --- a/obp-api/src/main/resources/props/test.default.props.template +++ b/obp-api/src/main/resources/props/test.default.props.template @@ -39,6 +39,19 @@ write_metrics = false # --for tests don't set it to 127.0.0.1, for some reason hostname=http://localhost:8016 +# Used as the base URL for password reset and email validation links sent via email. +# Set this to your frontend/portal URL so that emails contain the correct link. +portal_external_url=http://localhost:5174 + +# Set to true to skip email validation on user signup (default: false) +authUser.skipEmailValidation=false + +# Expiry time in minutes for email validation JWT tokens (default: 1440 = 24 hours) +email_validation_token_expiry_minutes=1440 + +# Expiry time in minutes for password reset JWT tokens (default: 120 = 2 hours) +password_reset_token_expiry_minutes=120 + #this is only useful for running the api locally via RunWebApp #if you use it, make sure this matches your hostname port! #if you want to change the port when running via the command line, use "mvn -Djetty.port=8089 jetty:run" instead diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala index faeba636a1..47f95f6bc1 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala @@ -194,8 +194,8 @@ object MessageDocsSwaggerDefinitions title =titleExample.value, branchId = branchIdExample.value, nameSuffix = nameSuffixExample.value, - customerType = "", - parentCustomerId = "" + customerType = Some("INDIVIDUAL"), + parentCustomerId = Some("") ) val customerAttribute = CustomerAttributeCommons( diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index c27548ee8a..773e26ebf0 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -2135,8 +2135,7 @@ object SwaggerDefinitionsJSON { username = usernameExample.value, password = "String", first_name = "Simon", - last_name = "Redfern", - validating_application = Some("OBP-Portal") + last_name = "Redfern" ) diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index 7b7553eba4..7a691e90c1 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -1114,7 +1114,7 @@ object Glossary extends MdcLoggable { glossaryItems += GlossaryItem( - title = "Consent_OBP_Flow_Example", + title = "Authentication: Consent OBP Flow Example", description = s""" |#### 1) Call endpoint Create Consent Request using application access (Client Credentials) @@ -1355,7 +1355,7 @@ object Glossary extends MdcLoggable { glossaryItems += GlossaryItem( - title = "Direct Login", + title = "Authentication: Direct Login", description = s""" |Direct Login is a simple authentication process to be used at hackathons and trusted environments: @@ -2136,7 +2136,7 @@ object Glossary extends MdcLoggable { """) glossaryItems += GlossaryItem( - title = "OAuth 1.0a", + title = "Authentication: OAuth 1.0a", description = s""" |The following steps will explain how to connect an instance of the Open Bank Project OAuth Server 1.0a. This authentication mechanism is necessary so a third party application can consume the Open Bank project API securely. @@ -2372,7 +2372,7 @@ object Glossary extends MdcLoggable { {"OAuth2 is allowed on this instance."} else {"Note: *OAuth2 is NOT allowed on this instance!*"} glossaryItems += GlossaryItem( - title = "OAuth 2", + title = "Authentication: OAuth 2", description = s""" | @@ -2581,7 +2581,7 @@ object Glossary extends MdcLoggable { glossaryItems += GlossaryItem( - title = "Gateway Login", + title = "Authentication: Gateway Login", description = s""" |### Introduction @@ -4151,7 +4151,7 @@ object Glossary extends MdcLoggable { | """.stripMargin) glossaryItems += GlossaryItem( - title = "OAuth 2.0", + title = "Authentication: OAuth 2.0", description = s"""OAuth 2.0, is a framework, specified by the IETF in RFCs 6749 and 6750 (published in 2012) designed to support the development of authentication and authorization protocols. It provides a variety of standardized message flows based on JSON and HTTP.""".stripMargin) @@ -5037,6 +5037,105 @@ object Glossary extends MdcLoggable { ) ) + glossaryItems += GlossaryItem( + title = "Email Validation for OBP Local Users", + description = + s""" + |### Overview + | + |When a new OBP local user is created, they may be required to validate their email address before they can log in. + |This is controlled by the `authUser.skipEmailValidation` property (default: `false`). + | + |When email validation is enabled, the user receives an email containing a signed JWT token with a validation link. + |The user clicks the link, and the App (portal) extracts the token and calls the API to complete the validation. + | + |### Props + | + |The following properties are involved: + | + |- `authUser.skipEmailValidation` — Set to `true` to skip email validation entirely (default: `false`). Currently: `${APIUtil.getPropsAsBoolValue("authUser.skipEmailValidation", false)}` + |- `portal_external_url` — **Required.** The base URL of your frontend/portal application. Used to construct the validation link in the email. For example: `portal_external_url=https://your-portal.example.com`. Currently: `${APIUtil.getPropsValue("portal_external_url", "not set")}` + |- `email_validation_token_expiry_minutes` — Expiry time for the validation JWT token in minutes (default: `1440` i.e. 24 hours). Currently: `${APIUtil.getPropsAsIntValue("email_validation_token_expiry_minutes", 1440)}` + | + |### Step 1: User Creation + | + |A user can be created via: + | + |**POST /obp/v6.0.0/users** (no authentication required) + | + |Request body: + | + | { + | "username": "user@example.com", + | "password": "Str0ng!Password", + | "first_name": "Jane", + | "last_name": "Doe", + | "email": "user@example.com" + | } + | + |If `authUser.skipEmailValidation=false`, the API will: + | + |1. Create the user with `validated=false` + |2. Generate a signed JWT token containing the user's unique ID as the subject, with a configurable expiry + |3. Construct a validation link: `{portal_external_url}/user-validation?token={JWT}` + |4. Send an email to the user with the validation link + | + |The user or the legacy Lift signup form can also trigger validation emails. In all cases, the same JWT-based token is used. + | + |### Step 2: Email Validation + | + |**POST /obp/v6.0.0/users/email-validation** (no authentication required) + | + |Request body: + | + | { + | "token": "eyJhbGciOiJIUzI1NiJ9..." + | } + | + |Response (201): + | + | { + | "user_id": "5995d6a2-01b3-423c-a173-5481df49bdaf", + | "email": "user@example.com", + | "username": "user@example.com", + | "provider": "https://your-api.example.com", + | "validated": true, + | "message": "Email validated successfully" + | } + | + |Error responses: + | + |- **400** — Invalid JSON format or empty token + |- **404** — Invalid or expired JWT token (bad signature, expired, or user not found) + |- **400** — User email is already validated + | + |This endpoint: + | + |1. Verifies the JWT signature (HMAC) and checks the expiry time + |2. Extracts the unique ID from the JWT subject + |3. Looks up the user by unique ID + |4. Sets the user's validated status to `true` + |5. Resets the unique ID (invalidating the token — it is single-use) + |6. Grants default entitlements to the user + | + |### Token Security + | + |- The token is a **signed JWT** (HMAC-SHA256) — it cannot be forged without the server's shared secret. + |- The token has a **configurable expiry** (default: 24 hours) set via `email_validation_token_expiry_minutes`. + |- The token is **single-use** — after validation, the unique ID is reset, so the same token cannot be used again. + | + |### Typical App Flow + | + |1. User submits registration form + |2. App calls POST /obp/v6.0.0/users + |3. App shows "Check your email for a validation link" + |4. User clicks link in email, App opens at `/user-validation?token={JWT}` + |5. App extracts the token from the URL query parameter + |6. App calls POST /obp/v6.0.0/users/email-validation with the token + |7. App shows "Email validated successfully. Please log in." + | + |""") + glossaryItems += GlossaryItem( title = "Password Reset for OBP Local Users", description = @@ -5073,6 +5172,7 @@ object Glossary extends MdcLoggable { | |- The response is always the same whether or not the user exists. This prevents user enumeration. |- If the user exists, is validated, and the email matches, a reset email is sent containing a link with a reset token. + |- The reset link base URL is constructed from the `portal_external_url` props value (currently: `${APIUtil.getPropsValue("portal_external_url", "not set")}`). This must be set to your frontend/portal URL so that reset emails contain the correct link. |- The App should present a form asking for username and email, call this endpoint, and then show a message saying "Check your email for a reset link." | |### Step 2: Complete Password Reset @@ -5101,7 +5201,7 @@ object Glossary extends MdcLoggable { | |Notes: | - |- The token is a signed JWT with a configurable expiry (default: 120 minutes). The server-side expiry can be configured with the `password_reset_token_expiry_minutes` property. + |- The token is a signed JWT with a configurable expiry (default: 120 minutes). The server-side expiry can be configured with the `password_reset_token_expiry_minutes` property (currently: `${APIUtil.getPropsAsIntValue("password_reset_token_expiry_minutes", 120)}` minutes). |- The token comes from the reset email URL. The App should extract the token from the URL path (everything after `/user_mgt/reset_password/`) and URL-decode it before sending it to this endpoint. |- The token is single-use. Once the password is reset, the token is invalidated. An expired token will also be rejected. | @@ -5148,7 +5248,7 @@ object Glossary extends MdcLoggable { """) glossaryItems += GlossaryItem( - title = "Credential Checking Flow", + title = "Authentication: Credential Checking Flow", description = s""" |### Overview diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 2fcde5e0b4..b8c836ae9b 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -3213,7 +3213,7 @@ trait APIMethods600 { (_, callContext) <- NewStyle.function.getBank(bankId, cc.callContext) (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext) _ <- Helper.booleanToFuture(failMsg = CustomerTypeMismatch, 404, callContext) { - customer.customerType == "INDIVIDUAL" + customer.customerType.contains("INDIVIDUAL") } (customerAttributes, callContext) <- NewStyle.function.getCustomerAttributes( bankId, @@ -3402,7 +3402,7 @@ trait APIMethods600 { (_, callContext) <- NewStyle.function.getBank(bankId, cc.callContext) (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext) _ <- Helper.booleanToFuture(failMsg = CustomerTypeMismatch, 404, callContext) { - List("CORPORATE", "SUBSIDIARY").contains(customer.customerType) + customer.customerType.exists(ct => List("CORPORATE", "SUBSIDIARY").contains(ct)) } (customerAttributes, callContext) <- NewStyle.function.getCustomerAttributes( bankId, @@ -3447,7 +3447,7 @@ trait APIMethods600 { (_, callContext) <- NewStyle.function.getBank(bankId, cc.callContext) (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext) _ <- Helper.booleanToFuture(failMsg = CustomerTypeMismatch, 404, callContext) { - List("CORPORATE", "SUBSIDIARY").contains(customer.customerType) + customer.customerType.exists(ct => List("CORPORATE", "SUBSIDIARY").contains(ct)) } (children, callContext) <- NewStyle.function.getCustomersByParentCustomerId(bankId, customerId, callContext) } yield { @@ -3811,15 +3811,16 @@ trait APIMethods600 { "POST", "/users/email-validation", "Validate User Email", - s"""Validate a user's email address using the token sent via email. + s"""Validate a user's email address using the JWT token sent via email. | |This endpoint is called anonymously (no authentication required). | |When a user signs up and email validation is enabled (authUser.skipEmailValidation=false), - |they receive an email with a validation link containing a unique token. + |they receive an email with a validation link containing a signed JWT token. | |This endpoint: - |- Validates the token + |- Verifies the JWT signature and checks expiry + |- Extracts the unique ID from the JWT subject |- Sets the user's validated status to true |- Resets the unique ID token (invalidating the link) |- Grants default entitlements to the user @@ -3827,16 +3828,12 @@ trait APIMethods600 { |**Important: This is a single-use token.** Once the email is validated, the token is invalidated. |Any subsequent attempts to use the same token will return a 404 error (UserNotFoundByToken or UserAlreadyValidated). | - |The token is a unique identifier (UUID) that was generated when the user was created. - | - |Example token from validation email URL: - |https://your-obp-instance.com/user_mgt/validate_user/a1b2c3d4-e5f6-7890-abcd-ef1234567890 - | - |In this case, the token would be: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + |The token is a signed JWT with a configurable expiry (default: 1440 minutes / 24 hours). + |The server-side expiry can be configured with the `email_validation_token_expiry_minutes` property. | |""".stripMargin, JSONFactory600.ValidateUserEmailJsonV600( - token = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + token = "eyJhbGciOiJIUzI1NiJ9..." ), JSONFactory600.ValidateUserEmailResponseJsonV600( user_id = "5995d6a2-01b3-423c-a173-5481df49bdaf", @@ -3867,9 +3864,25 @@ trait APIMethods600 { _ <- Helper.booleanToFuture(s"$InvalidJsonFormat Token cannot be empty", cc = cc.callContext) { token.nonEmpty } - // Find user by unique ID (the validation token) + // Verify JWT signature and extract uniqueId from subject + uniqueId <- NewStyle.function.tryons( + s"$UserNotFoundByToken Invalid or expired validation token", + 404, + cc.callContext + ) { + val signedJWT = com.nimbusds.jwt.SignedJWT.parse(token) + val expiration = signedJWT.getJWTClaimsSet.getExpirationTime + if (expiration == null || expiration.before(new java.util.Date())) { + throw new Exception("Token has expired") + } + if (!CertificateUtil.verifywtWithHmacProtection(token)) { + throw new Exception("Invalid token signature") + } + signedJWT.getJWTClaimsSet.getSubject + } + // Find user by unique ID from JWT authUser <- Future { - code.model.dataAccess.AuthUser.findUserByValidationToken(token) match { + code.model.dataAccess.AuthUser.findUserByValidationToken(uniqueId) match { case Full(user) => Full(user) case Empty => Empty case f: net.liftweb.common.Failure => f @@ -4243,11 +4256,6 @@ trait APIMethods600 { | | Requires username(email), password, first_name, last_name, and email. | - | Optional fields: - | - validating_application: Optional application name that will validate the user's email (e.g., "LEGACY_PORTAL") - | When set to "LEGACY_PORTAL", the validation link will use the API hostname property - | When set to any other value or not provided, the validation link will use the portal_external_url property (default behavior) - | | Validation checks performed: | - Password must meet strong password requirements (InvalidStrongPasswordFormat error if not) | - Username must be unique (409 error if username already exists) @@ -4256,9 +4264,7 @@ trait APIMethods600 { | Email validation behavior: | - Controlled by property 'authUser.skipEmailValidation' (default: false) | - When false: User is created with validated=false and a validation email is sent to the user's email address - | - Validation link domain is determined by validating_application: - | * "LEGACY_PORTAL": Uses API hostname property (e.g., https://api.example.com) - | * Other/None (default): Uses portal_external_url property (e.g., https://external-portal.example.com) + | - The validation link is constructed using the `portal_external_url` property which must be set | - When true: User is created with validated=true and no validation email is sent | - Default entitlements are granted immediately regardless of validation status | @@ -4319,40 +4325,36 @@ trait APIMethods600 { // STEP 8: Send validation email (if required) val skipEmailValidation = APIUtil.getPropsAsBoolValue("authUser.skipEmailValidation", defaultValue = false) if (!skipEmailValidation) { - // Construct validation link based on validating_application and portal_external_url - val portalExternalUrl = APIUtil.getPropsValue("portal_external_url") + APIUtil.getPropsValue("portal_external_url") match { + case Full(portalUrl) => + // Create a JWT token with the uniqueId as subject and configurable expiry + val expiryMinutes = APIUtil.getPropsAsIntValue("email_validation_token_expiry_minutes", 1440) + val claimsSet = new com.nimbusds.jwt.JWTClaimsSet.Builder() + .subject(savedUser.uniqueId.get) + .expirationTime(new java.util.Date(System.currentTimeMillis() + expiryMinutes * 60L * 1000L)) + .issueTime(new java.util.Date()) + .build() + val jwtToken = CertificateUtil.jwtWithHmacProtection(claimsSet) + + val emailValidationLink = portalUrl + "/user-validation?token=" + java.net.URLEncoder.encode(jwtToken, "UTF-8") + + val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $emailValidationLink") + val htmlContent = Some(s"

Welcome! Please validate your account by clicking the following link:

$emailValidationLink

") + val subjectContent = "Sign up confirmation" + + val emailContent = code.api.util.CommonsEmailWrapper.EmailContent( + from = code.model.dataAccess.AuthUser.emailFrom, + to = List(savedUser.email.get), + bcc = code.model.dataAccess.AuthUser.bccEmail.toList, + subject = subjectContent, + textContent = textContent, + htmlContent = htmlContent + ) - val emailValidationLink = postedData.validating_application match { - case Some("LEGACY_PORTAL") => - // Use API hostname with legacy path - Constant.HostName + "/" + code.model.dataAccess.AuthUser.validateUserPath.mkString("/") + "/" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8") + code.api.util.CommonsEmailWrapper.sendHtmlEmail(emailContent) case _ => - // If portal_external_url is set, use modern portal path - // Otherwise fall back to API hostname with legacy path - portalExternalUrl match { - case Full(portalUrl) => - // Portal is configured - use modern frontend route - portalUrl + "/user-validation?token=" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8") - case _ => - // No portal configured - fall back to API hostname with legacy path - Constant.HostName + "/" + code.model.dataAccess.AuthUser.validateUserPath.mkString("/") + "/" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8") - } + logger.error("portal_external_url is not set in props. Cannot send validation email.") } - - val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $emailValidationLink") - val htmlContent = Some(s"

Welcome! Please validate your account by clicking the following link:

$emailValidationLink

") - val subjectContent = "Sign up confirmation" - - val emailContent = code.api.util.CommonsEmailWrapper.EmailContent( - from = code.model.dataAccess.AuthUser.emailFrom, - to = List(savedUser.email.get), - bcc = code.model.dataAccess.AuthUser.bccEmail.toList, - subject = subjectContent, - textContent = textContent, - htmlContent = htmlContent - ) - - code.api.util.CommonsEmailWrapper.sendHtmlEmail(emailContent) } // STEP 9: Grant default entitlements @@ -5439,6 +5441,10 @@ trait APIMethods600 { // Explicitly type the user to ensure proper method resolution val user: code.model.dataAccess.AuthUser = authUser + val portalUrl = APIUtil.getPropsValue("portal_external_url").openOrThrowException( + "portal_external_url is not set in props. It is required to construct the password reset link." + ) + // Generate new reset token user.uniqueId.set(java.util.UUID.randomUUID().toString.replace("-", "")) user.save @@ -5453,7 +5459,7 @@ trait APIMethods600 { val jwtToken = CertificateUtil.jwtWithHmacProtection(claimsSet) // Construct reset URL using portal_external_url - val resetPasswordLink = APIUtil.getPropsValue("portal_external_url", Constant.HostName) + + val resetPasswordLink = portalUrl + "/reset-password/" + java.net.URLEncoder.encode(jwtToken, "UTF-8") @@ -5545,8 +5551,8 @@ trait APIMethods600 { net.liftweb.mapper.By(code.model.dataAccess.AuthUser.username, postedData.username) ) - authUserBox match { - case Full(user) if user.validated.get && user.email.get == postedData.email => + (authUserBox, APIUtil.getPropsValue("portal_external_url")) match { + case (Full(user), Full(portalUrl)) if user.validated.get && user.email.get == postedData.email => // Generate new reset token user.uniqueId.set(java.util.UUID.randomUUID().toString.replace("-", "")) user.save @@ -5561,7 +5567,7 @@ trait APIMethods600 { val jwtToken = CertificateUtil.jwtWithHmacProtection(claimsSet) // Construct reset URL - val resetPasswordLink = APIUtil.getPropsValue("portal_external_url", Constant.HostName) + + val resetPasswordLink = portalUrl + "/reset-password/" + java.net.URLEncoder.encode(jwtToken, "UTF-8") @@ -5581,6 +5587,9 @@ trait APIMethods600 { code.api.util.CommonsEmailWrapper.sendHtmlEmail(emailContent) + case (_, Empty) => + logger.error("portal_external_url is not set in props. Cannot send password reset email.") + case _ => // Do nothing - return same response to prevent user enumeration } @@ -8794,6 +8803,9 @@ trait APIMethods600 { consumer_id = Some(consumer.consumerId.get), redirect_uris = redirectUris ), HttpCode.`200`(callContext)) + case Full(consumer) if !consumer.isActive.get => + logger.warn(s"verifyOidcClient: client_id ${postedData.client_id} exists but is not active (consumer_id: ${consumer.consumerId.get})") + (VerifyOidcClientResponseJsonV600(valid = false), HttpCode.`200`(callContext)) case _ => (VerifyOidcClientResponseJsonV600(valid = false), HttpCode.`200`(callContext)) } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index febdb37f48..74dc3fb029 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -252,8 +252,7 @@ case class CreateUserJsonV600( username: String, password: String, first_name: String, - last_name: String, - validating_application: Option[String] = None + last_name: String ) case class PostVerifyUserCredentialsJsonV600( @@ -1310,8 +1309,8 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { title = cInfo.title, branch_id = cInfo.branchId, name_suffix = cInfo.nameSuffix, - customer_type = cInfo.customerType, - parent_customer_id = cInfo.parentCustomerId + customer_type = cInfo.customerType.getOrElse("INDIVIDUAL"), + parent_customer_id = cInfo.parentCustomerId.getOrElse("") ) } @@ -1362,8 +1361,8 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { title = cInfo.title, branch_id = cInfo.branchId, name_suffix = cInfo.nameSuffix, - customer_type = cInfo.customerType, - parent_customer_id = cInfo.parentCustomerId, + customer_type = cInfo.customerType.getOrElse("INDIVIDUAL"), + parent_customer_id = cInfo.parentCustomerId.getOrElse(""), customer_attributes = customerAttributes.map(customerAttribute => CustomerAttributeResponseJsonV300( customer_attribute_id = customerAttribute.customerAttributeId, diff --git a/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala b/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala index 017ce18066..6a8dc2f1c9 100644 --- a/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala +++ b/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala @@ -3837,8 +3837,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -3890,8 +3890,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -3944,8 +3944,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4007,8 +4007,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4057,8 +4057,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4108,8 +4108,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4408,8 +4408,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4459,8 +4459,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5164,8 +5164,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), exampleInboundMessage = ( InBoundGetCustomerAttributesForCustomers(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala index 3d16eeee4d..aa9f84e430 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala @@ -4534,8 +4534,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4587,8 +4587,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4641,8 +4641,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4704,8 +4704,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4754,8 +4754,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4804,8 +4804,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4855,8 +4855,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5155,8 +5155,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5206,8 +5206,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5942,8 +5942,8 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), exampleInboundMessage = ( InBoundGetCustomerAttributesForCustomers(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index 3ecb945a52..d1701e8c57 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -4464,8 +4464,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4517,8 +4517,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4571,8 +4571,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4634,8 +4634,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4684,8 +4684,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4734,8 +4734,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4785,8 +4785,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5085,8 +5085,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5136,8 +5136,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5841,8 +5841,8 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), exampleInboundMessage = ( InBoundGetCustomerAttributesForCustomers(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, diff --git a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala index b3a5b54d36..4421183ab4 100644 --- a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala @@ -4669,8 +4669,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4722,8 +4722,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4776,8 +4776,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4839,8 +4839,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4889,8 +4889,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4939,8 +4939,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -4990,8 +4990,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId="")) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some(""))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5290,8 +5290,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -5341,8 +5341,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -6077,8 +6077,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { title=customerTitleExample.value, branchId=branchIdExample.value, nameSuffix=nameSuffixExample.value, - customerType="", - parentCustomerId=""))) + customerType=Some("INDIVIDUAL"), + parentCustomerId=Some("")))) ), exampleInboundMessage = ( InBoundGetCustomerAttributesForCustomers(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, diff --git a/obp-api/src/main/scala/code/customer/MappedCustomerProvider.scala b/obp-api/src/main/scala/code/customer/MappedCustomerProvider.scala index de644968a2..ce8aeb867c 100644 --- a/obp-api/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/obp-api/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -435,8 +435,8 @@ class MappedCustomer extends Customer with Agent with LongKeyedMapper[MappedCust override def title: String = mTitle.get override def branchId: String = mBranchId.get override def nameSuffix: String = mNameSuffix.get - override def customerType: String = mCustomerType.get - override def parentCustomerId: String = mParentCustomerId.get + override def customerType: Option[String] = Option(mCustomerType.get) + override def parentCustomerId: Option[String] = Option(mParentCustomerId.get) override def isConfirmedAgent: Boolean = mIsConfirmedAgent.get //This is for Agent diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index 2e98b89a3b..4694aeffc4 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -663,26 +663,40 @@ import net.liftweb.util.Helpers._ * Overridden to use the hostname set in the props file */ override def sendValidationEmail(user: TheUserType) { - val resetLink = Constant.HostName+"/"+validateUserPath.mkString("/")+"/"+urlEncode(user.getUniqueId()) - val email: String = user.getEmail - val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $resetLink") - val htmlContent = Some(s"

Welcome! Please validate your account by clicking the following link:

$resetLink

") - val subjectContent = "Sign up confirmation" - val emailContent = EmailContent( - from = emailFrom, - to = List(user.getEmail), - bcc = bccEmail.toList, - subject = subjectContent, - textContent = textContent, - htmlContent = htmlContent - ) - sendHtmlEmail(emailContent) match { - case Full(messageId) => - logger.debug(s"Validation email sent successfully with Message-ID: $messageId") - S.notice("Validation email sent successfully. Please check your email.") - case Empty => - logger.error("Failed to send validation email") - S.error("Failed to send validation email. Please try again.") + APIUtil.getPropsValue("portal_external_url") match { + case Full(portalUrl) => + // Create a JWT token with the uniqueId as subject and configurable expiry + val expiryMinutes = APIUtil.getPropsAsIntValue("email_validation_token_expiry_minutes", 1440) + val claimsSet = new com.nimbusds.jwt.JWTClaimsSet.Builder() + .subject(user.getUniqueId()) + .expirationTime(new java.util.Date(System.currentTimeMillis() + expiryMinutes * 60L * 1000L)) + .issueTime(new java.util.Date()) + .build() + val jwtToken = CertificateUtil.jwtWithHmacProtection(claimsSet) + val validationLink = portalUrl+"/user-validation?token="+urlEncode(jwtToken) + val email: String = user.getEmail + val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $validationLink") + val htmlContent = Some(s"

Welcome! Please validate your account by clicking the following link:

$validationLink

") + val subjectContent = "Sign up confirmation" + val emailContent = EmailContent( + from = emailFrom, + to = List(user.getEmail), + bcc = bccEmail.toList, + subject = subjectContent, + textContent = textContent, + htmlContent = htmlContent + ) + sendHtmlEmail(emailContent) match { + case Full(messageId) => + logger.debug(s"Validation email sent successfully with Message-ID: $messageId") + S.notice("Validation email sent successfully. Please check your email.") + case Empty => + logger.error("Failed to send validation email") + S.error("Failed to send validation email. Please try again.") + } + case _ => + logger.error("portal_external_url is not set in props. Cannot send validation email.") + S.error("Validation email could not be sent. Please contact the administrator.") } } @@ -693,23 +707,40 @@ import net.liftweb.util.Helpers._ } } - override def validateUser(id: String): NodeSeq = findUserByUniqueId(id) match { - case Full(user) if !user.validated_? => - user.setValidated(true).resetUniqueId().save - grantDefaultEntitlementsToAuthUser(user) - logUserIn(user, () => { - S.notice(S.?("account.validated")) - APIUtil.getPropsValue("user_account_validated_redirect_url") match { - case Full(redirectUrl) => - logger.debug(s"user_account_validated_redirect_url = $redirectUrl") - S.redirectTo(redirectUrl) - case _ => - logger.debug(s"user_account_validated_redirect_url is NOT defined") - S.redirectTo(homePage) - } - }) + override def validateUser(id: String): NodeSeq = { + // Extract uniqueId from JWT token: verify signature and expiry + val uniqueIdBox: Box[String] = tryo { + val signedJWT = com.nimbusds.jwt.SignedJWT.parse(id) + val expiration = signedJWT.getJWTClaimsSet.getExpirationTime + if (expiration == null || expiration.before(new java.util.Date())) { + throw new Exception("Token has expired") + } + if (!CertificateUtil.verifywtWithHmacProtection(id)) { + throw new Exception("Invalid token signature") + } + signedJWT.getJWTClaimsSet.getSubject + } + + val userBox = uniqueIdBox.flatMap(findUserByUniqueId) - case _ => S.error(S.?("invalid.validation.link")); S.redirectTo(homePage) + userBox match { + case Full(user) if !user.validated_? => + user.setValidated(true).resetUniqueId().save + grantDefaultEntitlementsToAuthUser(user) + logUserIn(user, () => { + S.notice(S.?("account.validated")) + APIUtil.getPropsValue("user_account_validated_redirect_url") match { + case Full(redirectUrl) => + logger.debug(s"user_account_validated_redirect_url = $redirectUrl") + S.redirectTo(redirectUrl) + case _ => + logger.debug(s"user_account_validated_redirect_url is NOT defined") + S.redirectTo(homePage) + } + }) + + case _ => S.error(S.?("invalid.validation.link")); S.redirectTo(homePage) + } } override def actionsAfterSignup(theUser: TheUserType, func: () => Nothing): Nothing = { diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala index 93253e5f82..b79636ad90 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala @@ -168,8 +168,8 @@ case class CustomerCommons( title :String, branchId :String, nameSuffix :String, - customerType :String = "INDIVIDUAL", - parentCustomerId :String = "") extends Customer + customerType :Option[String] = Some("INDIVIDUAL"), + parentCustomerId :Option[String] = Some("")) extends Customer object CustomerCommons extends Converter[Customer, CustomerCommons] diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/CustomerDataModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/CustomerDataModel.scala index 4fed90cb92..724252d11a 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/CustomerDataModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/CustomerDataModel.scala @@ -53,8 +53,8 @@ trait Customer { def title: String def branchId: String def nameSuffix: String - def customerType: String // "INDIVIDUAL", "CORPORATE", "SUBSIDIARY" - def parentCustomerId: String // customerId of parent, or "" if none + def customerType: Option[String] // "INDIVIDUAL", "CORPORATE", "SUBSIDIARY" + def parentCustomerId: Option[String] // customerId of parent, or None if none } trait Agent {