@@ -25,6 +25,7 @@ import com.tesobe.oidc.endpoints.HtmlUtils.htmlEncode
2525import com .tesobe .oidc .models .{OidcError , User }
2626import com .tesobe .oidc .ratelimit .RateLimitService
2727import com .tesobe .oidc .config .OidcConfig
28+ import com .tesobe .oidc .tokens .JwtService
2829import org .http4s ._
2930import org .http4s .dsl .io ._
3031import org .http4s .headers .Location
@@ -36,7 +37,8 @@ class AuthEndpoint(
3637 codeService : CodeService [IO ],
3738 statsService : StatsService [IO ],
3839 rateLimitService : RateLimitService [IO ],
39- config : OidcConfig
40+ config : OidcConfig ,
41+ jwtService : JwtService [IO ]
4042) {
4143
4244 private val logger = LoggerFactory .getLogger(getClass)
@@ -109,12 +111,12 @@ class AuthEndpoint(
109111 )
110112 ) *>
111113 // Validate request parameters
112- (if (responseType != " code" ) {
114+ (if (responseType != " code" && responseType != " code id_token " ) {
113115 IO (logger.warn(s " Unsupported response_type: $responseType" )) *>
114116 IO (println(s " Unsupported response_type: $responseType" )) *> {
115117 val error = OidcError (
116118 " unsupported_response_type" ,
117- Some (" Only 'code' response type is supported " ),
119+ Some (" Supported response types: 'code', 'code id_token' " ),
118120 state = state
119121 )
120122 redirectWithError(redirectUri, error)
@@ -163,7 +165,7 @@ class AuthEndpoint(
163165 logger.info(s " Client validated, showing login form... " )
164166 ) *>
165167 IO (println(s " Client validated, showing login form... " )) *>
166- showLoginForm(clientId, redirectUri, scope, state, nonce)
168+ showLoginForm(clientId, redirectUri, scope, state, nonce, responseType = responseType )
167169 }
168170 }
169171 })
@@ -268,6 +270,7 @@ class AuthEndpoint(
268270 )
269271 state = formData.get(" state" )
270272 nonce = formData.get(" nonce" )
273+ responseType = formData.get(" response_type" ).getOrElse(" code" )
271274
272275 _ <- IO (
273276 logger.info(
@@ -295,7 +298,8 @@ class AuthEndpoint(
295298 redirectUri,
296299 scope,
297300 state,
298- nonce
301+ nonce,
302+ responseType
299303 )
300304 case Left (error) =>
301305 // Authentication failed - record failed attempt for rate limiting
@@ -316,7 +320,8 @@ class AuthEndpoint(
316320 scope,
317321 state,
318322 nonce,
319- Some (" Incorrect username/password" )
323+ Some (" Incorrect username/password" ),
324+ responseType
320325 )
321326 }
322327 } yield response
@@ -334,13 +339,22 @@ class AuthEndpoint(
334339 redirectUri : String ,
335340 scope : String ,
336341 state : Option [String ],
337- nonce : Option [String ]
342+ nonce : Option [String ],
343+ responseType : String = " code"
338344 ): IO [Response [IO ]] = {
339345 for {
340346 _ <- statsService.incrementLoginSuccess(user.username)
341347 code <- codeService
342348 .generateCode(clientId, redirectUri, user.sub, scope, state, nonce, user.provider)
343- response <- redirectWithCode(redirectUri, code, state)
349+ response <- responseType match {
350+ case " code id_token" =>
351+ for {
352+ idToken <- jwtService.generateHybridIdToken(user, clientId, code, state, nonce)
353+ resp <- redirectWithCodeAndIdToken(redirectUri, code, idToken, state)
354+ } yield resp
355+ case _ =>
356+ redirectWithCode(redirectUri, code, state)
357+ }
344358 } yield response
345359 }
346360
@@ -350,7 +364,8 @@ class AuthEndpoint(
350364 scope : String ,
351365 state : Option [String ],
352366 nonce : Option [String ],
353- errorMessage : Option [String ] = None
367+ errorMessage : Option [String ] = None ,
368+ responseType : String = " code"
354369 ): IO [Response [IO ]] = {
355370
356371 IO (logger.info(s " showLoginForm called for clientId: $clientId" )) *>
@@ -484,6 +499,7 @@ class AuthEndpoint(
484499 <input type="hidden" name="client_id" value=" ${htmlEncode(clientId)}">
485500 <input type="hidden" name="redirect_uri" value=" ${htmlEncode(redirectUri)}">
486501 <input type="hidden" name="scope" value=" ${htmlEncode(scope)}">
502+ <input type="hidden" name="response_type" value=" ${htmlEncode(responseType)}">
487503 $stateParam
488504 $nonceParam
489505
@@ -614,6 +630,23 @@ class AuthEndpoint(
614630 SeeOther (Location (Uri .unsafeFromString(location)))
615631 }
616632
633+ /** Redirect with both code and id_token in the fragment (hybrid flow).
634+ * Per OIDC Core 3.3.2.5, when response_type includes a token or id_token,
635+ * parameters MUST be returned in the URI fragment.
636+ */
637+ private def redirectWithCodeAndIdToken (
638+ redirectUri : String ,
639+ code : String ,
640+ idToken : String ,
641+ state : Option [String ]
642+ ): IO [Response [IO ]] = {
643+ val stateParam = state.map(s => s " &state= ${java.net.URLEncoder .encode(s, " UTF-8" )}" ).getOrElse(" " )
644+ val location = s " $redirectUri#code= $code&id_token= $idToken$stateParam"
645+ IO (logger.info(s " Redirecting with code and id_token (hybrid flow) to: ${redirectUri}#code=...&id_token=... " )) *>
646+ IO (println(s " Redirecting with code and id_token (hybrid flow) " )) *>
647+ SeeOther (Location (Uri .unsafeFromString(location)))
648+ }
649+
617650 private def redirectWithError (
618651 redirectUri : String ,
619652 error : OidcError
@@ -634,13 +667,15 @@ object AuthEndpoint {
634667 codeService : CodeService [IO ],
635668 statsService : StatsService [IO ],
636669 rateLimitService : RateLimitService [IO ],
637- config : OidcConfig
670+ config : OidcConfig ,
671+ jwtService : JwtService [IO ]
638672 ): AuthEndpoint =
639673 new AuthEndpoint (
640674 authService,
641675 codeService,
642676 statsService,
643677 rateLimitService,
644- config
678+ config,
679+ jwtService
645680 )
646681}
0 commit comments