Skip to content

Commit 1d02693

Browse files
committed
Cleaning up after Hola work
1 parent 4d96fce commit 1d02693

2 files changed

Lines changed: 39 additions & 80 deletions

File tree

src/main/scala/com/tesobe/oidc/endpoints/AuthEndpoint.scala

Lines changed: 36 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

src/main/scala/com/tesobe/oidc/models/OidcModels.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,19 +213,19 @@ object RefreshTokenClaims {
213213
implicit val decoder: Decoder[RefreshTokenClaims] = deriveDecoder
214214
}
215215

216-
// Consent challenge: holds paused authorization state while user approves consent in Portal
216+
// Consent challenge: holds paused authorization state while user approves consent in Portal.
217+
// No user identity at creation time — the user authenticates on Portal, not here.
218+
// Portal passes back user_id on the callback so we can generate the auth code.
217219
case class ConsentChallenge(
218220
challenge: String, // Unique ID for this challenge
219221
clientId: String, // Hola's client_id
220222
redirectUri: String, // Hola's redirect_uri
221-
sub: String, // Authenticated user's subject ID
222223
scope: String, // Requested scopes
223224
state: Option[String], // Hola's OAuth state
224225
nonce: Option[String], // Hola's OAuth nonce
225226
responseType: String, // "code" or "code id_token"
226227
consentRequestId: String, // OBP consent_request_id
227228
bankId: String, // OBP bank_id
228-
provider: Option[String], // Auth provider used
229229
exp: Long // Expiration time
230230
)
231231

0 commit comments

Comments
 (0)