@@ -88,8 +88,9 @@ class AuthEndpoint(
8888 case GET -> Root / " obp-oidc" / " consent-callback" :?
8989 ChallengeQueryParamMatcher (challengeId) +&
9090 ConsentIdCallbackQueryParamMatcher (consentId) +&
91- ConsentStatusQueryParamMatcher (consentStatus) =>
92- handleConsentCallback(challengeId, consentId, consentStatus)
91+ ConsentStatusQueryParamMatcher (consentStatus) +&
92+ UserIdCallbackQueryParamMatcher (userId) =>
93+ handleConsentCallback(challengeId, consentId, consentStatus, userId)
9394 }
9495
9596 // Query parameter matchers
@@ -117,6 +118,8 @@ class AuthEndpoint(
117118 extends OptionalQueryParamDecoderMatcher [String ](" consent_id" )
118119 object ConsentStatusQueryParamMatcher
119120 extends QueryParamDecoderMatcher [String ](" consent_status" )
121+ object UserIdCallbackQueryParamMatcher
122+ extends OptionalQueryParamDecoderMatcher [String ](" user_id" )
120123
121124 private def handleAuthorizationRequest (
122125 responseType : String ,
@@ -189,12 +192,18 @@ class AuthEndpoint(
189192 redirectWithError(redirectUri, error)
190193 }
191194 } else {
192- // Show login form
193- IO (
194- logger.info(s " Client validated, showing login form... " )
195- ) *>
196- IO (println(s " Client validated, showing login form... " )) *>
197- showLoginForm(clientId, redirectUri, scope, state, nonce, responseType = responseType, consentRequestId = consentRequestId, bankId = bankId)
195+ consentRequestId match {
196+ case Some (crId) =>
197+ // Consent flow: skip login form, redirect straight to Portal
198+ // The user will authenticate on Portal (which does its own OAuth with OBP-OIDC)
199+ IO (logger.info(s " Client validated, consent_request_id present — skipping login, redirecting to Portal... " )) *>
200+ redirectToPortalForConsent(clientId, redirectUri, scope, state, nonce, responseType, crId, bankId.getOrElse(" " ))
201+ case None =>
202+ // Normal flow: show login form
203+ IO (logger.info(s " Client validated, showing login form... " )) *>
204+ IO (println(s " Client validated, showing login form... " )) *>
205+ showLoginForm(clientId, redirectUri, scope, state, nonce, responseType = responseType)
206+ }
198207 }
199208 }
200209 })
@@ -300,8 +309,6 @@ class AuthEndpoint(
300309 state = formData.get(" state" )
301310 nonce = formData.get(" nonce" )
302311 responseType = formData.get(" response_type" ).getOrElse(" code" )
303- consentRequestId = formData.get(" consent_request_id" ).filter(_.nonEmpty)
304- bankId = formData.get(" bank_id" ).filter(_.nonEmpty)
305312
306313 _ <- IO (
307314 logger.info(
@@ -323,20 +330,10 @@ class AuthEndpoint(
323330 IO (
324331 logger.info(s " Authentication successful for user: ${user.sub}" )
325332 ) *>
326- // If consent_request_id is present, redirect to Portal for consent approval
327- // instead of generating the auth code immediately
328- (consentRequestId match {
329- case Some (crId) =>
330- redirectToPortalForConsent(
331- user, clientId, redirectUri, scope, state, nonce,
332- responseType, crId, bankId.getOrElse(" " ), user.provider
333- )
334- case None =>
335- generateCodeForUser(
336- user, clientId, redirectUri, scope, state, nonce,
337- responseType, consentId = None
338- )
339- })
333+ generateCodeForUser(
334+ user, clientId, redirectUri, scope, state, nonce,
335+ responseType, consentId = None
336+ )
340337 case Left (error) =>
341338 // Authentication failed - record failed attempt for rate limiting
342339 rateLimitService.checkAndRecordFailedAttempt(ip, validUsername) *>
@@ -357,9 +354,7 @@ class AuthEndpoint(
357354 state,
358355 nonce,
359356 Some (" Incorrect username/password" ),
360- responseType,
361- consentRequestId,
362- bankId
357+ responseType
363358 )
364359 }
365360 } yield response
@@ -371,20 +366,18 @@ class AuthEndpoint(
371366 BadRequest (" Invalid form data. Please try again." )
372367 }
373368
374- /** After successful authentication, if there's a consent_request_id,
375- * store the authorization state and redirect to Portal for consent approval .
369+ /** Store authorization state and redirect to Portal for consent approval.
370+ * No user authentication happens here — the user will authenticate on Portal .
376371 */
377372 private def redirectToPortalForConsent (
378- user : User ,
379373 clientId : String ,
380374 redirectUri : String ,
381375 scope : String ,
382376 state : Option [String ],
383377 nonce : Option [String ],
384378 responseType : String ,
385379 consentRequestId : String ,
386- bankId : String ,
387- provider : Option [String ]
380+ bankId : String
388381 ): IO [Response [IO ]] = {
389382 for {
390383 challengeId <- IO (UUID .randomUUID().toString)
@@ -394,14 +387,12 @@ class AuthEndpoint(
394387 challenge = challengeId,
395388 clientId = clientId,
396389 redirectUri = redirectUri,
397- sub = user.sub,
398390 scope = scope,
399391 state = state,
400392 nonce = nonce,
401393 responseType = responseType,
402394 consentRequestId = consentRequestId,
403395 bankId = bankId,
404- provider = provider,
405396 exp = exp
406397 )
407398
@@ -425,12 +416,14 @@ class AuthEndpoint(
425416 }
426417
427418 /** Handle the consent callback from Portal after user approves/denies consent.
428- * Look up the stored ConsentChallenge, generate the auth code, and redirect to Hola.
419+ * No OAuth token exchange needed — Hola uses Consent-Id + Consumer-Key headers
420+ * to access OBP-API. We just redirect back to Hola with the consent_id.
429421 */
430422 private def handleConsentCallback (
431423 challengeId : String ,
432424 consentId : Option [String ],
433- consentStatus : String
425+ consentStatus : String ,
426+ userId : Option [String ]
434427 ): IO [Response [IO ]] = {
435428 for {
436429 challenges <- consentChallengesRef.get
@@ -447,25 +440,12 @@ class AuthEndpoint(
447440 }
448441 } else if (consentStatus == " ACCEPTED" || consentStatus == " VALID" ) {
449442 IO (logger.info(s " Consent approved for challenge: $challengeId, consent_id: $consentId" )) *> {
450- // Look up the user and generate the auth code with consent_id
451- authService.getUserById(challenge.sub).flatMap {
452- case Some (user) =>
453- generateCodeForUser(
454- user,
455- challenge.clientId,
456- challenge.redirectUri,
457- challenge.scope,
458- challenge.state,
459- challenge.nonce,
460- challenge.responseType,
461- consentId
462- )
463- case None =>
464- IO (logger.error(s " User not found for sub: ${challenge.sub}" )) *> {
465- val error = OidcError (" server_error" , Some (" User not found" ), state = challenge.state)
466- redirectWithError(challenge.redirectUri, error)
467- }
468- }
443+ // Redirect back to Hola with the consent_id — no OAuth code/token needed
444+ val consentIdParam = consentId.map(c => s " &consent_id= ${java.net.URLEncoder .encode(c, " UTF-8" )}" ).getOrElse(" " )
445+ val stateParam = challenge.state.map(s => s " &state= ${java.net.URLEncoder .encode(s, " UTF-8" )}" ).getOrElse(" " )
446+ val location = s " ${challenge.redirectUri}?consent_status=ACCEPTED ${consentIdParam}${stateParam}"
447+ IO (logger.info(s " Redirecting to Hola with consent_id: $location" )) *>
448+ SeeOther (Location (Uri .unsafeFromString(location)))
469449 }
470450 } else {
471451 IO (logger.info(s " Consent denied for challenge: $challengeId, status: $consentStatus" )) *> {
@@ -514,9 +494,7 @@ class AuthEndpoint(
514494 state : Option [String ],
515495 nonce : Option [String ],
516496 errorMessage : Option [String ] = None ,
517- responseType : String = " code" ,
518- consentRequestId : Option [String ] = None ,
519- bankId : Option [String ] = None
497+ responseType : String = " code"
520498 ): IO [Response [IO ]] = {
521499
522500 IO (logger.info(s " showLoginForm called for clientId: $clientId" )) *>
@@ -531,12 +509,6 @@ class AuthEndpoint(
531509 nonceParam = nonce
532510 .map(n => s """ <input type="hidden" name="nonce" value=" ${htmlEncode(n)}"> """ )
533511 .getOrElse(" " )
534- consentRequestIdParam = consentRequestId
535- .map(c => s """ <input type="hidden" name="consent_request_id" value=" ${htmlEncode(c)}"> """ )
536- .getOrElse(" " )
537- bankIdParam = bankId
538- .map(b => s """ <input type="hidden" name="bank_id" value=" ${htmlEncode(b)}"> """ )
539- .getOrElse(" " )
540512
541513 providerOptions = providers
542514 .map { provider =>
@@ -558,16 +530,6 @@ class AuthEndpoint(
558530 .mkString(" " )
559531 .replace(" Obp " , " OBP " ))
560532
561- // Show a colored banner so you can tell which login step you're on
562- consentBannerHtml = consentRequestId match {
563- case Some (crId) =>
564- s """ <div style="background: #fef3c7; border: 2px solid #f59e0b; border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; text-align: center;">
565- <strong style="color: #92400e;">Consent Authorization</strong><br>
566- <span style="color: #78350f; font-size: 0.9rem;">Sign in to authorize a consent request ${bankId.map(b => s " for bank <strong> ${htmlEncode(b)}</strong> " ).getOrElse(" " )}</span>
567- </div> """
568- case None => " "
569- }
570-
571533 errorHtml = errorMessage
572534 .map(msg => s """ <div class="error"> $msg</div> """ )
573535 .getOrElse(" " )
@@ -614,7 +576,6 @@ class AuthEndpoint(
614576 $logoHtml
615577 <h2>Sign In</h2>
616578 <p class="subtitle"> $formattedClientName is asking you to login</p>
617- $consentBannerHtml
618579 $errorHtml
619580 ${if (config.localDevelopmentMode) {
620581 s """ <div class="info">
@@ -670,8 +631,6 @@ class AuthEndpoint(
670631 <input type="hidden" name="response_type" value=" ${htmlEncode(responseType)}">
671632 $stateParam
672633 $nonceParam
673- $consentRequestIdParam
674- $bankIdParam
675634
676635 <button type="submit">Sign In</button>
677636 </form>
0 commit comments