Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .sdkmanrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=11.0.28-tem
75 changes: 68 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ This project is dual licensed under the AGPL V3 (see NOTICE) and commercial lice

## Setup

### Installing JDK
#### With sdkman

A good way to manage JDK versions and install the correct version for OBP is [sdkman](https://sdkman.io/). If you have this installed then you can install the correct JDK easily using:
```
sdk env install
```

#### Manually

- OracleJDK: 1.8, 13
- OpenJdk: 11

OpenJDK 11 is available for download here: [https://jdk.java.net/archive/](https://jdk.java.net/archive/).

The project uses Maven 3 as its build tool.

To compile and run Jetty, install Maven 3, create your configuration in `obp-api/src/main/resources/props/default.props` and execute:
Expand Down Expand Up @@ -666,6 +681,59 @@ allow_oauth2_login=true
oauth2.jwk_set.url=https://www.googleapis.com/oauth2/v3/certs
```

### OAuth2 JWKS URI Configuration

The `oauth2.jwk_set.url` property is critical for OAuth2 JWT token validation. OBP-API uses this to verify the authenticity of JWT tokens by fetching the JSON Web Key Set (JWKS) from the specified URI(s).

#### Configuration Methods

The `oauth2.jwk_set.url` property is resolved in the following order of priority:

1. **Environment Variable**

```bash
export OBP_OAUTH2_JWK_SET_URL="https://your-oidc-server.com/jwks"
```

2. **Properties Files** (located in `obp-api/src/main/resources/props/`)
- `production.default.props` (for production deployments)
- `default.props` (for development)
- `test.default.props` (for testing)

#### Supported Formats

- **Single URL**: `oauth2.jwk_set.url=http://localhost:9000/obp-oidc/jwks`
- **Multiple URLs**: `oauth2.jwk_set.url=http://localhost:8080/jwk.json,https://www.googleapis.com/oauth2/v3/certs`

#### Common OAuth2 Provider Examples

- **Google**: `https://www.googleapis.com/oauth2/v3/certs`
- **OBP-OIDC**: `http://localhost:9000/obp-oidc/jwks`
- **Keycloak**: `http://localhost:7070/realms/master/protocol/openid-connect/certs`
- **Azure AD**: `https://login.microsoftonline.com/common/discovery/v2.0/keys`

#### Troubleshooting OBP-20208 Error

If you encounter the error "OBP-20208: Cannot match the issuer and JWKS URI at this server instance", check the following:

1. **Verify JWT Issuer Claim**: The JWT token's `iss` (issuer) claim must match one of the configured identity providers
2. **Check JWKS URL Configuration**: Ensure `oauth2.jwk_set.url` contains URLs that correspond to your JWT issuer
3. **Case-Insensitive Matching**: OBP-API performs case-insensitive substring matching between the issuer and JWKS URLs
4. **URL Format Consistency**: Check for trailing slashes or URL formatting differences

**Debug Logging**: Enable debug logging to see detailed information about the matching process:

```properties
# Add to your logging configuration
logger.code.api.OAuth2=DEBUG
```

The debug logs will show:

- Expected identity provider vs actual JWT issuer claim
- Available JWKS URIs from configuration
- Matching logic results

---

## Frozen APIs
Expand Down Expand Up @@ -700,13 +768,6 @@ The same as `Frozen APIs`, if a related unit test fails, make sure whether the m

- A good book on Lift: "Lift in Action" by Timothy Perrett published by Manning.

## Supported JDK Versions

- OracleJDK: 1.8, 13
- OpenJdk: 11

OpenJDK 11 is available for download here: [https://jdk.java.net/archive/](https://jdk.java.net/archive/).

## Endpoint Request and Response Example

```log
Expand Down
101 changes: 97 additions & 4 deletions obp-api/src/main/scala/code/api/OAuth2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,65 @@ object OAuth2Login extends RestHelper with MdcLoggable {
def checkUrlOfJwkSets(identityProvider: String) = {
val url: List[String] = Constant.oauth2JwkSetUrl.toList
val jwksUris: List[String] = url.map(_.toLowerCase()).map(_.split(",").toList).flatten


logger.debug(s"checkUrlOfJwkSets - identityProvider: '$identityProvider'")
logger.debug(s"checkUrlOfJwkSets - oauth2.jwk_set.url raw value: '${Constant.oauth2JwkSetUrl}'")
logger.debug(s"checkUrlOfJwkSets - parsed jwksUris: $jwksUris")

// Enhanced matching for both URL-based and semantic identifiers
val identityProviderLower = identityProvider.toLowerCase()
val jwksUri = jwksUris.filter(_.contains(identityProviderLower))


logger.debug(s"checkUrlOfJwkSets - identityProviderLower: '$identityProviderLower'")
logger.debug(s"checkUrlOfJwkSets - filtered jwksUri: $jwksUri")

jwksUri match {
case x :: _ => Full(x)
case Nil => Failure(Oauth2CannotMatchIssuerAndJwksUriException)
case x :: _ =>
logger.debug(s"checkUrlOfJwkSets - SUCCESS: Found matching JWKS URI: '$x'")
Full(x)
case Nil =>
logger.debug(s"checkUrlOfJwkSets - FAILURE: Cannot match issuer '$identityProvider' with any JWKS URI")
logger.debug(s"checkUrlOfJwkSets - Expected issuer pattern: '$identityProvider' (case-insensitive contains match)")
logger.debug(s"checkUrlOfJwkSets - Available JWKS URIs: $jwksUris")
logger.debug(s"checkUrlOfJwkSets - Identity provider (lowercase): '$identityProviderLower'")
logger.debug(s"checkUrlOfJwkSets - Matching logic: Looking for JWKS URIs containing '$identityProviderLower'")
Failure(Oauth2CannotMatchIssuerAndJwksUriException)
}
}

def checkUrlOfJwkSetsWithToken(identityProvider: String, jwtToken: String) = {
val actualIssuer = JwtUtil.getIssuer(jwtToken).getOrElse("NO_ISSUER_CLAIM")
val url: List[String] = Constant.oauth2JwkSetUrl.toList
val jwksUris: List[String] = url.map(_.toLowerCase()).map(_.split(",").toList).flatten

logger.debug(s"checkUrlOfJwkSetsWithToken - Expected identity provider: '$identityProvider'")
logger.debug(s"checkUrlOfJwkSetsWithToken - Actual JWT issuer claim: '$actualIssuer'")
logger.debug(s"checkUrlOfJwkSetsWithToken - oauth2.jwk_set.url raw value: '${Constant.oauth2JwkSetUrl}'")
logger.debug(s"checkUrlOfJwkSetsWithToken - parsed jwksUris: $jwksUris")

// Enhanced matching for both URL-based and semantic identifiers
val identityProviderLower = identityProvider.toLowerCase()
val jwksUri = jwksUris.filter(_.contains(identityProviderLower))

logger.debug(s"checkUrlOfJwkSetsWithToken - identityProviderLower: '$identityProviderLower'")
logger.debug(s"checkUrlOfJwkSetsWithToken - filtered jwksUri: $jwksUri")

jwksUri match {
case x :: _ =>
logger.debug(s"checkUrlOfJwkSetsWithToken - SUCCESS: Found matching JWKS URI: '$x'")
Full(x)
case Nil =>
logger.debug(s"checkUrlOfJwkSetsWithToken - FAILURE: Cannot match issuer with any JWKS URI")
logger.debug(s"checkUrlOfJwkSetsWithToken - Expected identity provider: '$identityProvider'")
logger.debug(s"checkUrlOfJwkSetsWithToken - Actual JWT issuer claim: '$actualIssuer'")
logger.debug(s"checkUrlOfJwkSetsWithToken - Available JWKS URIs: $jwksUris")
logger.debug(s"checkUrlOfJwkSetsWithToken - Expected pattern (lowercase): '$identityProviderLower'")
logger.debug(s"checkUrlOfJwkSetsWithToken - Matching logic: Looking for JWKS URIs containing '$identityProviderLower'")
logger.debug(s"checkUrlOfJwkSetsWithToken - TROUBLESHOOTING:")
logger.debug(s"checkUrlOfJwkSetsWithToken - 1. Verify oauth2.jwk_set.url contains URL matching '$identityProvider'")
logger.debug(s"checkUrlOfJwkSetsWithToken - 2. Check if JWT issuer '$actualIssuer' should match identity provider '$identityProvider'")
logger.debug(s"checkUrlOfJwkSetsWithToken - 3. Ensure case-insensitive substring matching works: does any JWKS URI contain '$identityProviderLower'?")
Failure(Oauth2CannotMatchIssuerAndJwksUriException)
}
}

Expand All @@ -259,14 +310,33 @@ object OAuth2Login extends RestHelper with MdcLoggable {
}.getOrElse(false)
}
def validateIdToken(idToken: String): Box[IDTokenClaimsSet] = {
logger.debug(s"validateIdToken - attempting to validate ID token")

// Extract issuer for better error reporting
val actualIssuer = JwtUtil.getIssuer(idToken).getOrElse("NO_ISSUER_CLAIM")
logger.debug(s"validateIdToken - JWT issuer claim: '$actualIssuer'")

urlOfJwkSets match {
case Full(url) =>
logger.debug(s"validateIdToken - using JWKS URL: '$url'")
JwtUtil.validateIdToken(idToken, url)
case ParamFailure(a, b, c, apiFailure : APIFailure) =>
logger.debug(s"validateIdToken - ParamFailure: $a, $b, $c, $apiFailure")
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
ParamFailure(a, b, c, apiFailure : APIFailure)
case Failure(msg, t, c) =>
logger.debug(s"validateIdToken - Failure getting JWKS URL: $msg")
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
if (msg.contains("OBP-20208")) {
logger.debug("validateIdToken - OBP-20208 Error Details:")
logger.debug(s"validateIdToken - JWT issuer claim: '$actualIssuer'")
logger.debug(s"validateIdToken - oauth2.jwk_set.url value: '${Constant.oauth2JwkSetUrl}'")
logger.debug("validateIdToken - Check that the JWKS URL configuration matches the JWT issuer")
}
Failure(msg, t, c)
case _ =>
logger.debug("validateIdToken - No JWKS URL available")
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
Failure(Oauth2ThereIsNoUrlOfJwkSet)
}
}
Expand Down Expand Up @@ -414,19 +484,42 @@ object OAuth2Login extends RestHelper with MdcLoggable {
}

def applyIdTokenRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = {
logger.debug("applyIdTokenRules - starting ID token validation")

// Extract issuer from token for debugging
val actualIssuer = JwtUtil.getIssuer(token).getOrElse("NO_ISSUER_CLAIM")
logger.debug(s"applyIdTokenRules - JWT issuer claim: '$actualIssuer'")

validateIdToken(token) match {
case Full(_) =>
logger.debug("applyIdTokenRules - ID token validation successful")
val user = getOrCreateResourceUser(token)
val consumer = getOrCreateConsumer(token, user.map(_.userId), Some(OpenIdConnect.openIdConnect))
LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
case true => ((Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))))
case false => (user, Some(cc.copy(consumer = consumer)))
}
case ParamFailure(a, b, c, apiFailure : APIFailure) =>
logger.debug(s"applyIdTokenRules - ParamFailure during token validation: $a")
logger.debug(s"applyIdTokenRules - JWT issuer was: '$actualIssuer'")
(ParamFailure(a, b, c, apiFailure : APIFailure), Some(cc))
case Failure(msg, t, c) =>
logger.debug(s"applyIdTokenRules - Failure during token validation: $msg")
logger.debug(s"applyIdTokenRules - JWT issuer was: '$actualIssuer'")
if (msg.contains("OBP-20208")) {
logger.debug("applyIdTokenRules - OBP-20208: JWKS URI matching failed. Diagnostic info:")
logger.debug(s"applyIdTokenRules - Actual JWT issuer: '$actualIssuer'")
logger.debug(s"applyIdTokenRules - oauth2.jwk_set.url config: '${Constant.oauth2JwkSetUrl}'")
logger.debug("applyIdTokenRules - Resolution steps:")
logger.debug("1. Verify oauth2.jwk_set.url contains URLs that match the JWT issuer")
logger.debug("2. Check if JWT issuer claim matches expected identity provider")
logger.debug("3. Ensure case-insensitive substring matching works between issuer and JWKS URLs")
logger.debug("4. Consider if trailing slashes or URL formatting might be causing mismatch")
}
(Failure(msg, t, c), Some(cc))
case _ =>
logger.debug("applyIdTokenRules - Unknown failure during token validation")
logger.debug(s"applyIdTokenRules - JWT issuer was: '$actualIssuer'")
(Failure(Oauth2IJwtCannotBeVerified), Some(cc))
}
}
Expand Down