Skip to content

Commit a597b19

Browse files
committed
Fixing use endpoints method
1 parent 519ae58 commit a597b19

11 files changed

Lines changed: 319 additions & 81 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ OBP-OIDC supports two modes for verifying users, clients, and listing providers,
224224
- Verifies clients via `GET /obp/v6.0.0/oidc/clients/CLIENT_ID`
225225
- Lists providers via `GET /obp/v6.0.0/providers`
226226
- Useful when you don't want to grant direct database access to OBP-OIDC
227-
- Requires `OBP_API_USERNAME` to have `CanVerifyUserCredentials`, `CanGetOidcClient` and `CanGetConsumers` roles
227+
- Requires `OBP_API_USERNAME` to have `CanVerifyUserCredentials`, `CanGetAnyUser`, `CanGetOidcClient` and `CanGetConsumers` roles
228228
- When combined with `OIDC_SKIP_CLIENT_BOOTSTRAP=true`, no database connection is needed at all
229229

230230
**Configuration:**
@@ -236,7 +236,7 @@ USE_VERIFY_ENDPOINTS=false
236236
# Alternative: Use OBP API endpoints
237237
USE_VERIFY_ENDPOINTS=true
238238
OBP_API_URL=http://localhost:8080
239-
OBP_API_USERNAME=admin_user # Needs CanVerifyUserCredentials + CanGetOidcClient + CanGetConsumers roles
239+
OBP_API_USERNAME=admin_user # Needs CanVerifyUserCredentials + CanGetAnyUser + CanGetOidcClient + CanGetConsumers roles
240240
OBP_API_PASSWORD=admin_password
241241
OBP_API_CONSUMER_KEY=your_consumer_key
242242
```

src/main/scala/com/tesobe/oidc/auth/AuthService.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ trait AuthService[F[_]] {
3939
*/
4040
def getUserById(sub: String): F[Option[User]]
4141

42+
/** Get user information by subject ID and provider.
43+
* When a provider is available, this can use the OBP API
44+
* (GET /obp/v6.0.0/users/provider/PROVIDER/username/USERNAME)
45+
* instead of requiring a database connection.
46+
*/
47+
def getUserBySubAndProvider(sub: String, provider: String): F[Option[User]]
48+
4249
/** Get available authentication providers
4350
*/
4451
def getAvailableProviders(): F[List[String]]

src/main/scala/com/tesobe/oidc/auth/CodeService.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ trait CodeService[F[_]] {
3636
sub: String,
3737
scope: String,
3838
state: Option[String] = None,
39-
nonce: Option[String] = None
39+
nonce: Option[String] = None,
40+
provider: Option[String] = None
4041
): F[String]
4142
def validateAndConsumeCode(
4243
code: String,
@@ -58,7 +59,8 @@ class InMemoryCodeService(
5859
sub: String,
5960
scope: String,
6061
state: Option[String] = None,
61-
nonce: Option[String] = None
62+
nonce: Option[String] = None,
63+
provider: Option[String] = None
6264
): IO[String] = {
6365
for {
6466
code <- IO(UUID.randomUUID().toString.replace("-", ""))
@@ -75,6 +77,7 @@ class InMemoryCodeService(
7577
scope = scope,
7678
state = state,
7779
nonce = nonce,
80+
provider = provider,
7881
exp = exp
7982
)
8083

src/main/scala/com/tesobe/oidc/auth/HybridAuthService.scala

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
package com.tesobe.oidc.auth
2121

22-
import cats.effect.{IO, Resource}
22+
import cats.effect.{IO, Ref, Resource}
2323
import cats.implicits._
2424
import com.tesobe.oidc.config.{DatabaseConfig, DbVendor, ListProvidersMethod, OidcConfig, VerifyCredentialsMethod, VerifyClientMethod}
2525
import com.tesobe.oidc.models.{User, UserInfo, OidcError, OidcClient}
@@ -50,7 +50,8 @@ class HybridAuthService(
5050
adminTransactor: Option[Transactor[IO]] = None,
5151
config: OidcConfig,
5252
obpApiCredentialsService: Option[ObpApiCredentialsService] = None,
53-
obpApiClientService: Option[ObpApiClientService] = None
53+
obpApiClientService: Option[ObpApiClientService] = None,
54+
userCacheRef: Option[Ref[IO, Map[String, User]]] = None
5455
) extends AuthService[IO] {
5556

5657
private val logger = LoggerFactory.getLogger(getClass)
@@ -150,7 +151,7 @@ class HybridAuthService(
150151
)
151152

152153
// Check which credential validation method to use
153-
config.verifyCredentialsMethod match {
154+
val result = config.verifyCredentialsMethod match {
154155
case VerifyCredentialsMethod.ViaApiEndpoint =>
155156
logger.info(
156157
s"Using OBP API endpoint for credential verification (verify_credentials_endpoint)"
@@ -186,6 +187,20 @@ class HybridAuthService(
186187
)
187188
authenticateViaDatabase(username, password, provider)
188189
}
190+
191+
// Cache the user on successful authentication (for getUserById in API-only mode)
192+
result.flatMap {
193+
case Right(user) =>
194+
userCacheRef match {
195+
case Some(ref) =>
196+
ref.update(_ + (user.sub -> user)) *>
197+
IO(logger.info(s"Cached authenticated user: ${user.sub}")) *>
198+
IO.pure(Right(user))
199+
case None =>
200+
IO.pure(Right(user))
201+
}
202+
case left => IO.pure(left)
203+
}
189204
}
190205

191206
/** Authenticate a user via the v_oidc_users database view
@@ -323,7 +338,42 @@ class HybridAuthService(
323338
/** Get user by ID (for UserInfo endpoint) - required by AuthService interface
324339
*/
325340
def getUserById(sub: String): IO[Option[User]] = {
326-
findUserByUserId(sub).map(_.map(_.toUser))
341+
// When database is available, query it directly
342+
if (transactor.isDefined) {
343+
findUserByUserId(sub).map(_.map(_.toUser))
344+
} else {
345+
// In API-only mode, use the user cache populated during authentication
346+
userCacheRef match {
347+
case Some(ref) =>
348+
ref.get.map(_.get(sub)).flatTap {
349+
case Some(_) => IO(logger.info(s"getUserById: Found user '$sub' in cache"))
350+
case None => IO(logger.warn(s"getUserById: User '$sub' not found in cache"))
351+
}
352+
case None =>
353+
IO(logger.warn(s"getUserById: No database and no user cache available for '$sub'")) *>
354+
IO.pure(None)
355+
}
356+
}
357+
}
358+
359+
/** Get user by subject ID and provider.
360+
* Uses OBP API when in API-only mode, falls back to database otherwise.
361+
*/
362+
def getUserBySubAndProvider(sub: String, provider: String): IO[Option[User]] = {
363+
if (transactor.isDefined) {
364+
// Database mode: query directly
365+
findUserByUserId(sub).map(_.map(_.toUser))
366+
} else {
367+
// API-only mode: call OBP API GET /obp/v6.0.0/users/provider/PROVIDER/username/USERNAME
368+
obpApiCredentialsService match {
369+
case Some(service) =>
370+
logger.info(s"getUserBySubAndProvider: Looking up user '$sub' with provider '$provider' via OBP API")
371+
service.getUserByProviderAndUsername(provider, sub)
372+
case None =>
373+
logger.warn(s"getUserBySubAndProvider: No OBP API credentials service available for user '$sub'")
374+
IO.pure(None)
375+
}
376+
}
327377
}
328378

329379
/** Get user information by username (for UserInfo endpoint)
@@ -1585,12 +1635,14 @@ object HybridAuthService {
15851635
) *>
15861636
Resource.pure[IO, Option[ObpApiClientService]](None)
15871637
}
1638+
userCache <- Resource.eval(Ref.of[IO, Map[String, User]](Map.empty))
15881639
service = new HybridAuthService(
15891640
readTransactor,
15901641
adminTransactor,
15911642
config,
15921643
obpApiService,
1593-
obpApiClientService
1644+
obpApiClientService,
1645+
Some(userCache)
15941646
)
15951647
_ <- Resource.eval(
15961648
IO(logger.info("HybridAuthService created successfully"))

src/main/scala/com/tesobe/oidc/auth/ObpApiClientService.scala

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,14 @@ class ObpApiClientService(
305305
case Right(token) =>
306306
val endpoint =
307307
s"${baseUrl.stripSuffix("/")}/obp/v6.0.0/oidc/clients/verify"
308+
val maskedSecret = if (clientSecret.length <= 4) "****"
309+
else clientSecret.take(2) + "*" * (clientSecret.length - 4) + clientSecret.takeRight(2)
308310
logger.info(
309311
s"Verifying client via OBP API: $endpoint for client_id: $clientId"
310312
)
313+
logger.info(
314+
s"Client secret being sent: length=${clientSecret.length}, masked=$maskedSecret"
315+
)
311316

312317
val requestBody = VerifyClientRequest(
313318
client_id = clientId,
@@ -512,28 +517,35 @@ class ObpApiClientService(
512517
.use { response =>
513518
response.status match {
514519
case Status.Ok =>
515-
response.as[Json].flatMap { json =>
516-
json.as[ConsumersResponse] match {
517-
case Right(consumersResp) =>
518-
val clients = consumersResp.consumers.map { c =>
519-
OidcClient(
520-
client_id = c.consumer_key,
521-
client_secret = None,
522-
consumer_id = c.consumer_id,
523-
client_name = c.app_name,
524-
redirect_uris = c.redirect_url.split("[,\\s]+").map(_.trim).filter(_.nonEmpty).toList,
525-
grant_types = List("authorization_code", "refresh_token"),
526-
response_types = List("code"),
527-
scopes = List("openid", "profile", "email"),
528-
token_endpoint_auth_method = "client_secret_basic",
529-
created_at = c.created
530-
)
520+
response.as[String].flatMap { rawBody =>
521+
logger.info(s"OBP API consumers response (raw): $rawBody")
522+
io.circe.parser.parse(rawBody) match {
523+
case Right(json) =>
524+
json.as[ConsumersResponse] match {
525+
case Right(consumersResp) =>
526+
val clients = consumersResp.consumers.map { c =>
527+
OidcClient(
528+
client_id = c.consumer_key,
529+
client_secret = None,
530+
consumer_id = c.consumer_id,
531+
client_name = c.app_name,
532+
redirect_uris = c.redirect_url.split("[,\\s]+").map(_.trim).filter(_.nonEmpty).toList,
533+
grant_types = List("authorization_code", "refresh_token"),
534+
response_types = List("code"),
535+
scopes = List("openid", "profile", "email"),
536+
token_endpoint_auth_method = "client_secret_basic",
537+
created_at = c.created
538+
)
539+
}
540+
logger.info(s"Found ${clients.size} consumers via OBP API")
541+
IO.pure(Right(clients))
542+
case Left(error) =>
543+
logger.error(s"Failed to parse consumers response: ${error.getMessage}. Raw body: $rawBody")
544+
IO.pure(Left(OidcError("server_error", Some(s"Failed to parse OBP API response: $rawBody"))))
531545
}
532-
logger.info(s"Found ${clients.size} consumers via OBP API")
533-
IO.pure(Right(clients))
534-
case Left(error) =>
535-
logger.error(s"Failed to parse consumers response: ${error.getMessage}")
536-
IO.pure(Left(OidcError("server_error", Some(s"Failed to parse response: ${error.getMessage}"))))
546+
case Left(parseError) =>
547+
logger.error(s"OBP API returned non-JSON response: $rawBody")
548+
IO.pure(Left(OidcError("server_error", Some(s"OBP API returned non-JSON: $rawBody"))))
537549
}
538550
}
539551

0 commit comments

Comments
 (0)