Merge pull request #2635 from hongwei1/develop

test/fixed
This commit is contained in:
Simon Redfern 2025-11-27 12:40:08 +01:00 committed by GitHub
commit 80cf296387
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 420 additions and 144 deletions

View File

@ -0,0 +1,276 @@
# Keycloak Onboarding Guide
## Overview
Keycloak is an open-source identity and access management solution that provides production-grade OpenID Connect (OIDC) and OAuth 2.0 authentication services for the OBP-API. It serves as a centralized authentication provider that enables secure user authentication, authorization, and user management for banking applications.
## What is Keycloak?
Keycloak is a comprehensive identity provider that offers:
- **Single Sign-On (SSO)** capabilities across multiple applications
- **Identity Federation** with external identity providers (Google, Facebook, LDAP, etc.)
- **User Management** with role-based access control
- **Multi-factor Authentication (MFA)** support
- **Standards Compliance** with OAuth 2.0, OpenID Connect, and SAML 2.0
## Prerequisites for Onboarding
### System Requirements
- Docker or Java 11+ for running Keycloak
- Network access to Keycloak instance (default: `localhost:7787`)
- Administrative access to configure realms and clients
### OBP-API Configuration
Before integrating with Keycloak, ensure your OBP-API instance has the following properties configured:
# Enable OAuth2 login
`allow_oauth2_login=true`
# Keycloak-specific configuration
`oauth2.oidc_provider=keycloak`
`oauth2.keycloak.host=http://localhost:7787`
`oauth2.keycloak.realm=master`
`oauth2.keycloak.issuer=http://localhost:7787/realms/master`
`oauth2.jwk_set.url=http://localhost:7787/realms/master/protocol/openid-connect/certs`
## Step-by-Step Onboarding Process
### 1. Setting Up Keycloak Instance
#### Option A: Using Docker (Recommended for Development)
The OBP project provides a pre-configured Keycloak Docker image available at:
**Docker Hub**: https://hub.docker.com/r/openbankproject/obp-keycloak/tags
##### Inspect Available Tags
First, check available image tags:
`docker search openbankproject/obp-keycloak`
Common available tags:
- `main-themed`: Latest themed version with OBP branding
- `latest`: Standard latest version
- `dev`: Development version
- Version-specific tags (e.g., `21.1.2-themed`)
##### Pull and Inspect the Image
# Pull the OBP-themed Keycloak image
`docker pull openbankproject/obp-keycloak:main-themed`
# Inspect image details
`docker inspect openbankproject/obp-keycloak:main-themed`
# View image layers and size
`docker images | grep openbankproject/obp-keycloak`
# Check image history
`docker history openbankproject/obp-keycloak:main-themed`
##### Basic Container Setup
# Run Keycloak container with basic configuration
`docker run -p 7787:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \`
` openbankproject/obp-keycloak:main-themed start-dev`
##### Advanced Container Setup with Persistent Data
# Create a volume for persistent data
`docker volume create keycloak_data`
# Run with persistent data and custom configuration
`docker run -d --name obp-keycloak \`
` -p 7787:8080 \`
` -e KEYCLOAK_ADMIN=admin \`
` -e KEYCLOAK_ADMIN_PASSWORD=admin \`
` -e KC_DB=h2-file \`
` -e KC_DB_URL_PATH=/opt/keycloak/data/keycloak \`
` -v keycloak_data:/opt/keycloak/data \`
` openbankproject/obp-keycloak:main-themed start-dev`
##### Container Management Commands
# Check container status
`docker ps | grep obp-keycloak`
# View container logs
`docker logs obp-keycloak`
# Follow logs in real-time
`docker logs -f obp-keycloak`
# Stop the container
`docker stop obp-keycloak`
# Start existing container
`docker start obp-keycloak`
# Remove container (data will be preserved in volume)
`docker rm obp-keycloak`
##### Troubleshooting Container Issues
# Execute commands inside running container
`docker exec -it obp-keycloak bash`
# Check container resource usage
`docker stats obp-keycloak`
# Inspect container configuration
`docker inspect obp-keycloak`
##### Environment Variables and Configuration
The OBP Keycloak image supports these key environment variables:
**Admin Configuration:**
- `KEYCLOAK_ADMIN`: Admin username (default: admin)
- `KEYCLOAK_ADMIN_PASSWORD`: Admin password
- `KC_PROXY`: Proxy mode (edge, reencrypt, passthrough)
**Database Configuration:**
- `KC_DB`: Database type (h2-file, postgres, mysql, mariadb)
- `KC_DB_URL`: Database connection URL
- `KC_DB_USERNAME`: Database username
- `KC_DB_PASSWORD`: Database password
**Network Configuration:**
- `KC_HOSTNAME`: External hostname for Keycloak
- `KC_HTTP_PORT`: HTTP port (default: 8080)
- `KC_HTTPS_PORT`: HTTPS port (default: 8443)
**Example with External Database:**
`docker run -d --name obp-keycloak \`
` -p 7787:8080 \`
` -e KEYCLOAK_ADMIN=admin \`
` -e KEYCLOAK_ADMIN_PASSWORD=securepassword \`
` -e KC_DB=postgres \`
` -e KC_DB_URL=jdbc:postgresql://localhost:5432/keycloak \`
` -e KC_DB_USERNAME=keycloak \`
` -e KC_DB_PASSWORD=keycloak_password \`
` openbankproject/obp-keycloak:main-themed start-dev`
#### Option B: Manual Installation
1. Download Keycloak from [keycloak.org](https://www.keycloak.org/)
2. Extract and navigate to the Keycloak directory
3. Start Keycloak in development mode:
`./bin/kc.sh start-dev --http-port=7787`
### 2. Initial Keycloak Configuration
#### Access Keycloak Admin Console
- Navigate to <a href="http://localhost:7787/admin" target="_blank">http://localhost:7787/admin</a>
- Login with admin credentials (admin/admin for Docker setup)
#### Create or Configure Realm
1. Select or create a realm (e.g., "obp" or use "master")
2. Configure realm settings:
- **SSL required**: None (for development) or External requests (for production)
- **User registration**: Enable if needed
- **Login with email**: Enable for email-based authentication
### 3. Configure OBP Client Application
#### Create Client
1. Navigate to **Clients** → **Create Client**
2. Set **Client ID**: `obp-client` (or your preferred identifier)
3. Set **Client Type**: `OpenID Connect`
4. Enable **Client authentication**
#### Configure Client Settings
- **Root URL**: <a href="getServerUrl" target="_blank">your OBP-API URL</a>
- **Valid redirect URIs**: <a href="getServerUrl/auth/openid-connect/callback" target="_blank">callback URL</a>
- **Web origins**: <a href="getServerUrl" target="_blank">OBP-API URL</a>
- **Access Type**: `confidential`
#### Retrieve Client Credentials
1. Go to **Clients****obp-client** → **Credentials**
2. Copy the **Client Secret** for OBP-API configuration
### 4. Update OBP-API Configuration
Add the following to your OBP-API properties file:
# OpenID Connect Client Configuration
`openid_connect_1.button_text=Keycloak Login`
`openid_connect_1.client_id=obp-client`
`openid_connect_1.client_secret=YOUR_CLIENT_SECRET_HERE`
`openid_connect_1.callback_url=getServerUrl/auth/openid-connect/callback`
`openid_connect_1.endpoint.discovery=http://localhost:7787/realms/master/.well-known/openid-configuration`
`openid_connect_1.endpoint.authorization=http://localhost:7787/realms/master/protocol/openid-connect/auth`
`openid_connect_1.endpoint.userinfo=http://localhost:7787/realms/master/protocol/openid-connect/userinfo`
`openid_connect_1.endpoint.token=http://localhost:7787/realms/master/protocol/openid-connect/token`
`openid_connect_1.endpoint.jwks_uri=http://localhost:7787/realms/master/protocol/openid-connect/certs`
`openid_connect_1.access_type_offline=true`
### 5. User Management Setup
#### Create Test Users
1. Navigate to **Users** → **Add User**
2. Fill in user details:
- **Username**: `testuser`
- **Email**: `testuser@example.com`
- **First Name** and **Last Name**
3. Set temporary password in **Credentials** tab
4. Assign appropriate roles if using role-based access control
#### Configure User Attributes (Optional)
Map additional user attributes that OBP-API might need:
- `email` (standard)
- `name` (standard)
- `preferred_username` (standard)
- Custom attributes as required by your banking application
### 6. Testing the Integration
#### Verify Configuration
1. Restart OBP-API after configuration changes
2. Navigate to <a href="getServerUrl" target="_blank">OBP-API login page</a>
3. Look for "Keycloak Login" button (or your configured button text)
#### Test Authentication Flow
1. Click the Keycloak login button
2. Should redirect to Keycloak login page
3. Login with test user credentials
4. Should redirect back to OBP-API with successful authentication
5. Verify user session and JWT token validity
## Production Considerations
### Security Best Practices
- **Use HTTPS** for all Keycloak and OBP-API communications
- **Strong passwords** for admin accounts
- **Regular updates** of Keycloak version
- **Backup** realm configurations and user data
- **Monitor** authentication logs for suspicious activity
### Scalability
- **Database**: Configure external database (PostgreSQL, MySQL) instead of H2
- **Clustering**: Set up Keycloak cluster for high availability
- **Load Balancing**: Use load balancer for multiple Keycloak instances
### Integration with Banking Systems
- **LDAP/Active Directory**: Integrate with existing user directories
- **Multi-factor Authentication**: Enable MFA for enhanced security
- **Compliance**: Ensure configuration meets banking regulatory requirements
## Troubleshooting Common Issues
### Authentication Failures
- **Check redirect URIs**: Ensure they match exactly in both Keycloak and OBP-API
- **Verify client credentials**: Confirm client ID and secret are correct
- **Check realm configuration**: Ensure realm name matches in all configurations
### JWT Token Issues
- **Issuer mismatch**: Verify `oauth2.keycloak.issuer` matches JWT `iss` claim
- **JWKS endpoint**: Confirm `oauth2.jwk_set.url` is accessible and returns valid keys
- **Token expiration**: Check token validity periods in Keycloak settings
### Network Connectivity
- **Firewall rules**: Ensure ports 7787 (Keycloak) and 8080 (OBP-API) are accessible
- **DNS resolution**: Verify hostnames resolve correctly
- **SSL certificates**: For HTTPS setups, ensure valid certificates
## Additional Resources
- <a href="https://www.keycloak.org/documentation" target="_blank">Keycloak Official Documentation</a>
- <a href="https://openid.net/connect/" target="_blank">OpenID Connect Specification</a>
- [OBP OAuth 2.0 Client Credentials Flow Manual](OAuth_2.0_Client_Credentials_Flow_Manual.md)
- [OBP OIDC Configuration Guide](../../../OBP_OIDC_Configuration_Guide.md)
## Support
For issues related to Keycloak integration with OBP-API:
1. Check the OBP-API logs for detailed error messages
2. Verify Keycloak server logs for authentication issues
3. Consult the OBP community forums or GitHub issues
4. Review the comprehensive troubleshooting section in the OBP documentation

View File

@ -2105,7 +2105,7 @@ trait APIMethods600 {
NewStyle.function.hasEntitlement("", u.userId, canCreateGroupsAtAllBanks, callContext)
}
group <- Future {
code.group.Group.group.vend.createGroup(
code.group.GroupTrait.group.vend.createGroup(
postJson.bank_id.filter(_.nonEmpty),
postJson.group_name,
postJson.group_description,
@ -2169,7 +2169,7 @@ trait APIMethods600 {
for {
(Full(u), callContext) <- authenticatedAccess(cc)
group <- Future {
code.group.Group.group.vend.getGroup(groupId)
code.group.GroupTrait.group.vend.getGroup(groupId)
} map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Group not found", 404)
}
@ -2254,15 +2254,15 @@ trait APIMethods600 {
}
groups <- bankIdFilter match {
case Some(bankId) =>
code.group.Group.group.vend.getGroupsByBankId(Some(bankId)) map {
code.group.GroupTrait.group.vend.getGroupsByBankId(Some(bankId)) map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get groups", 400)
}
case None if bankIdParam.isDefined =>
code.group.Group.group.vend.getGroupsByBankId(None) map {
code.group.GroupTrait.group.vend.getGroupsByBankId(None) map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get groups", 400)
}
case None =>
code.group.Group.group.vend.getAllGroups() map {
code.group.GroupTrait.group.vend.getAllGroups() map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get groups", 400)
}
}
@ -2333,7 +2333,7 @@ trait APIMethods600 {
json.extract[PutGroupJsonV600]
}
existingGroup <- Future {
code.group.Group.group.vend.getGroup(groupId)
code.group.GroupTrait.group.vend.getGroup(groupId)
} map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Group not found", 404)
}
@ -2344,7 +2344,7 @@ trait APIMethods600 {
NewStyle.function.hasEntitlement("", u.userId, canUpdateGroupsAtAllBanks, callContext)
}
updatedGroup <- Future {
code.group.Group.group.vend.updateGroup(
code.group.GroupTrait.group.vend.updateGroup(
groupId,
putJson.group_name,
putJson.group_description,
@ -2401,7 +2401,7 @@ trait APIMethods600 {
for {
(Full(u), callContext) <- authenticatedAccess(cc)
existingGroup <- Future {
code.group.Group.group.vend.getGroup(groupId)
code.group.GroupTrait.group.vend.getGroup(groupId)
} map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Group not found", 404)
}
@ -2412,7 +2412,7 @@ trait APIMethods600 {
NewStyle.function.hasEntitlement("", u.userId, canDeleteGroupsAtAllBanks, callContext)
}
deleted <- Future {
code.group.Group.group.vend.deleteGroup(groupId)
code.group.GroupTrait.group.vend.deleteGroup(groupId)
} map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot delete group", 400)
}

View File

@ -1,45 +1,112 @@
package code.group
import net.liftweb.common.Box
import net.liftweb.util.SimpleInjector
import code.util.MappedUUID
import net.liftweb.common.{Box, Empty, Full}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.tryo
import scala.concurrent.Future
import com.openbankproject.commons.ExecutionContext.Implicits.global
object Group extends SimpleInjector {
val group = new Inject(buildOne _) {}
object MappedGroupProvider extends GroupProvider {
def buildOne: GroupProvider = MappedGroupProvider
}
trait GroupProvider {
def createGroup(
override def createGroup(
bankId: Option[String],
groupName: String,
groupDescription: String,
listOfRoles: List[String],
isEnabled: Boolean
): Box[Group]
): Box[GroupTrait] = {
tryo {
Group.create
.BankId(bankId.getOrElse(""))
.GroupName(groupName)
.GroupDescription(groupDescription)
.ListOfRoles(listOfRoles.mkString(","))
.IsEnabled(isEnabled)
.saveMe()
}
}
def getGroup(groupId: String): Box[Group]
def getGroupsByBankId(bankId: Option[String]): Future[Box[List[Group]]]
def getAllGroups(): Future[Box[List[Group]]]
override def getGroup(groupId: String): Box[GroupTrait] = {
Group.find(By(Group.GroupId, groupId))
}
def updateGroup(
override def getGroupsByBankId(bankId: Option[String]): Future[Box[List[GroupTrait]]] = {
Future {
tryo {
bankId match {
case Some(id) =>
Group.findAll(By(Group.BankId, id))
case None =>
Group.findAll(By(Group.BankId, ""))
}
}
}
}
override def getAllGroups(): Future[Box[List[GroupTrait]]] = {
Future {
tryo {
Group.findAll()
}
}
}
override def updateGroup(
groupId: String,
groupName: Option[String],
groupDescription: Option[String],
listOfRoles: Option[List[String]],
isEnabled: Option[Boolean]
): Box[Group]
): Box[GroupTrait] = {
Group.find(By(Group.GroupId, groupId)).flatMap { group =>
tryo {
groupName.foreach(name => group.GroupName(name))
groupDescription.foreach(desc => group.GroupDescription(desc))
listOfRoles.foreach(roles => group.ListOfRoles(roles.mkString(",")))
isEnabled.foreach(enabled => group.IsEnabled(enabled))
group.saveMe()
}
}
}
def deleteGroup(groupId: String): Box[Boolean]
override def deleteGroup(groupId: String): Box[Boolean] = {
Group.find(By(Group.GroupId, groupId)).flatMap { group =>
tryo {
group.delete_!
}
}
}
}
trait Group {
def groupId: String
def bankId: Option[String]
def groupName: String
def groupDescription: String
def listOfRoles: List[String]
def isEnabled: Boolean
class Group extends GroupTrait with LongKeyedMapper[Group] with IdPK with CreatedUpdated {
def getSingleton = Group
object GroupId extends MappedUUID(this)
object BankId extends MappedString(this, 255) // Empty string for system-level groups
object GroupName extends MappedString(this, 255)
object GroupDescription extends MappedText(this)
object ListOfRoles extends MappedText(this) // Comma-separated list of roles
object IsEnabled extends MappedBoolean(this)
override def groupId: String = GroupId.get.toString
override def bankId: Option[String] = {
val id = BankId.get
if (id == null || id.isEmpty) None else Some(id)
}
override def groupName: String = GroupName.get
override def groupDescription: String = GroupDescription.get
override def listOfRoles: List[String] = {
val rolesStr = ListOfRoles.get
if (rolesStr == null || rolesStr.isEmpty) List.empty
else rolesStr.split(",").map(_.trim).filter(_.nonEmpty).toList
}
override def isEnabled: Boolean = IsEnabled.get
}
object Group extends Group with LongKeyedMetaMapper[Group] {
override def dbTableName = "Group" // define the DB table name
override def dbIndexes = Index(GroupId) :: Index(BankId) :: super.dbIndexes
}

View File

@ -0,0 +1,45 @@
package code.group
import net.liftweb.common.Box
import net.liftweb.util.SimpleInjector
import scala.concurrent.Future
object GroupTrait extends SimpleInjector {
val group = new Inject(buildOne _) {}
def buildOne: GroupProvider = MappedGroupProvider
}
trait GroupProvider {
def createGroup(
bankId: Option[String],
groupName: String,
groupDescription: String,
listOfRoles: List[String],
isEnabled: Boolean
): Box[GroupTrait]
def getGroup(groupId: String): Box[GroupTrait]
def getGroupsByBankId(bankId: Option[String]): Future[Box[List[GroupTrait]]]
def getAllGroups(): Future[Box[List[GroupTrait]]]
def updateGroup(
groupId: String,
groupName: Option[String],
groupDescription: Option[String],
listOfRoles: Option[List[String]],
isEnabled: Option[Boolean]
): Box[GroupTrait]
def deleteGroup(groupId: String): Box[Boolean]
}
trait GroupTrait {
def groupId: String
def bankId: Option[String]
def groupName: String
def groupDescription: String
def listOfRoles: List[String]
def isEnabled: Boolean
}

View File

@ -1,112 +0,0 @@
package code.group
import code.util.MappedUUID
import net.liftweb.common.{Box, Empty, Full}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.tryo
import scala.concurrent.Future
import com.openbankproject.commons.ExecutionContext.Implicits.global
object MappedGroupProvider extends GroupProvider {
override def createGroup(
bankId: Option[String],
groupName: String,
groupDescription: String,
listOfRoles: List[String],
isEnabled: Boolean
): Box[Group] = {
tryo {
MappedGroup.create
.BankId(bankId.getOrElse(""))
.GroupName(groupName)
.GroupDescription(groupDescription)
.ListOfRoles(listOfRoles.mkString(","))
.IsEnabled(isEnabled)
.saveMe()
}
}
override def getGroup(groupId: String): Box[Group] = {
MappedGroup.find(By(MappedGroup.GroupId, groupId))
}
override def getGroupsByBankId(bankId: Option[String]): Future[Box[List[Group]]] = {
Future {
tryo {
bankId match {
case Some(id) =>
MappedGroup.findAll(By(MappedGroup.BankId, id))
case None =>
MappedGroup.findAll(By(MappedGroup.BankId, ""))
}
}
}
}
override def getAllGroups(): Future[Box[List[Group]]] = {
Future {
tryo {
MappedGroup.findAll()
}
}
}
override def updateGroup(
groupId: String,
groupName: Option[String],
groupDescription: Option[String],
listOfRoles: Option[List[String]],
isEnabled: Option[Boolean]
): Box[Group] = {
MappedGroup.find(By(MappedGroup.GroupId, groupId)).flatMap { group =>
tryo {
groupName.foreach(name => group.GroupName(name))
groupDescription.foreach(desc => group.GroupDescription(desc))
listOfRoles.foreach(roles => group.ListOfRoles(roles.mkString(",")))
isEnabled.foreach(enabled => group.IsEnabled(enabled))
group.saveMe()
}
}
}
override def deleteGroup(groupId: String): Box[Boolean] = {
MappedGroup.find(By(MappedGroup.GroupId, groupId)).flatMap { group =>
tryo {
group.delete_!
}
}
}
}
class MappedGroup extends Group with LongKeyedMapper[MappedGroup] with IdPK with CreatedUpdated {
def getSingleton = MappedGroup
object GroupId extends MappedUUID(this)
object BankId extends MappedString(this, 255) // Empty string for system-level groups
object GroupName extends MappedString(this, 255)
object GroupDescription extends MappedText(this)
object ListOfRoles extends MappedText(this) // Comma-separated list of roles
object IsEnabled extends MappedBoolean(this)
override def groupId: String = GroupId.get.toString
override def bankId: Option[String] = {
val id = BankId.get
if (id == null || id.isEmpty) None else Some(id)
}
override def groupName: String = GroupName.get
override def groupDescription: String = GroupDescription.get
override def listOfRoles: List[String] = {
val rolesStr = ListOfRoles.get
if (rolesStr == null || rolesStr.isEmpty) List.empty
else rolesStr.split(",").map(_.trim).filter(_.nonEmpty).toList
}
override def isEnabled: Boolean = IsEnabled.get
}
object MappedGroup extends MappedGroup with LongKeyedMetaMapper[MappedGroup] {
override def dbTableName = "Group" // define the DB table name
override def dbIndexes = Index(GroupId) :: Index(BankId) :: super.dbIndexes
}