This is a bare bones OpenID Connect (OIDC) provider built with http4s and functional programming in Scala.
This implementation follows the same technology stack as OBP-API-II and integrates with PostgreSQL database for real user authentication.
It's meant to be used with OBP-API and apps such as the OBP Portal by developers.
For External Access: To run behind a TLS terminating proxy with HTTPS URLs, set OIDC_EXTERNAL_URL
environment variable:
export OIDC_EXTERNAL_URL="https://oidc.yourdomain.com"
./run-server.sh
Its not a production grade OIDC server. For that use Keyclock or Hydra etc.
If you're having trouble understanding OIDC with OBP this tool might help.
Designed to create clients for the OBP Apps as it starts up. It will print client_id, secrets and other info so you can copy and paste into your Props or env files.
Designed to read / write to the OBP Users and Consumers tables via SQL views defined in OBP-API
This application assumes you have an OBP database running locally.
The following script in OBP-API will create the SQL roles and views (2 or 3 of them) that this application needs to work.
If you have OBP source code locally you can run the file thus:
psql -h localhost -p 5432 -d sandbox -U obp -f workspace_2024/OBP-API-C/OBP-API/obp-api/src/main/scripts/sql/create_oidc_user_and_views.sql
or from with in psql thus
\i PATH-TO-OBP-API-SOURCE-CODE/obp-api/src/main/scripts/sql/create_oidc_user_and_views.sql
cp ./run-server.example.sh ./run-server.sh
Maybe this involves an export
# Export the generated environment variables
export OIDC_USER_PASSWORD=YourGeneratedPassword123!
export OIDC_ADMIN_PASSWORD=YourGeneratedAdminPass456#
# ... (other exports from generated config)
Now you can try and run the server
./run-server.sh
NOTE: you should make sure the OBP-API well known url returns the OBP-OIDC address.
That's it! 🎉 Copy the printed OIDC client configurations to your OBP projects.
- Java 11 or higher
- Maven 3.6+
- PostgreSQL database with OBP schema
- OBP authuser table with validated users
Ensure you have a PostgreSQL database with the OBP authuser table populated with users.
See above.
Set environment variables for database connection:
Read-Only Database User (for user authentication):
See the sql script.
-
Compile the project:
mvn clean compile
-
Run the server:
mvn exec:java -Dexec.mainClass="com.tesobe.oidc.server.OidcServer"
-
The server starts on http://localhost:9000 and automatically:
- Creates OIDC clients for OBP-API, Portal, Explorer II, and Opey II
- Prints complete client configurations for easy integration
- Tests all database connections
For easier configuration and running:
-
Copy the example script:
cp run-server.example.sh run-server.sh
-
Edit your database credentials:
vim run-server.sh # Edit the DB_* environment variables with your actual database settings
-
Make it executable and run:
chmod +x run-server.sh ./run-server.sh
The script will:
- ✅ Set all necessary environment variables
- ✅ Build the project
- ✅ Start the server with helpful output
- ✅ Show available endpoint URLs
- ✅ Print ready-to-use OBP-API configuration
The server runs on port 9000 by default. You can change this by setting the OIDC_PORT
environment variable:
# Run on default port 9000
mvn exec:java -Dexec.mainClass="com.tesobe.oidc.server.OidcServer"
# Run on custom port (e.g., 8080)
export OIDC_PORT=8080
mvn exec:java -Dexec.mainClass="com.tesobe.oidc.server.OidcServer"
# Or set inline
OIDC_PORT=8080 mvn exec:java -Dexec.mainClass="com.tesobe.oidc.server.OidcServer"
GET /.well-known/openid-configuration
Returns the OIDC discovery document with all endpoint URLs and supported features.
GET /auth?response_type=code&client_id=YOUR_CLIENT&redirect_uri=YOUR_REDIRECT&scope=openid%20profile%20email&state=ABC123
Shows HTML login form for user authentication. Supports authorization code flow.
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTH_CODE&redirect_uri=YOUR_REDIRECT&client_id=YOUR_CLIENT
Exchanges authorization code for ID token and access token.
GET /userinfo
Authorization: Bearer ACCESS_TOKEN
Returns user claims based on token scope.
GET /jwks
Returns JSON Web Key Set for token verification.
Once the server is running, test it with these curl commands:
# Health check
curl -v http://localhost:9000/health
# Expected: "OIDC Provider is running"
# Root welcome page
curl -v http://localhost:9000/
# Expected: HTML page with endpoint documentation
This is the standard OIDC well-known URL that clients use to discover your service:
curl http://localhost:9000/.well-known/openid-configuration
Expected JSON response:
{
"issuer": "http://localhost:9000",
"authorization_endpoint": "http://localhost:9000/auth",
"token_endpoint": "http://localhost:9000/token",
"userinfo_endpoint": "http://localhost:9000/userinfo",
"jwks_uri": "http://localhost:9000/jwks",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "profile", "email"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"claims_supported": ["sub", "name", "email", "email_verified"]
}
curl http://localhost:9000/jwks
Expected JSON response:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "your-key-id",
"n": "...",
"e": "AQAB"
}
]
}
curl "http://localhost:9000/auth?response_type=code&client_id=test-client&redirect_uri=https://example.com/callback&scope=openid%20profile%20email&state=test123"
Expected: HTML login form
Open these URLs in your browser:
- Welcome Page:
http://localhost:9000/
- Discovery:
http://localhost:9000/.well-known/openid-configuration
- JWKS:
http://localhost:9000/jwks
- Login Form:
http://localhost:9000/auth?response_type=code&client_id=test-client&redirect_uri=https://example.com/callback&scope=openid&state=test123
This OIDC provider uses three PostgreSQL database views:
- Authenticates users from the validated authuser table
- BCrypt password verification
- User profile information (name, email)
- Validates registered OIDC clients
- Controls allowed redirect URIs
- Manages client permissions and scopes
- Provides write access for client administration
- Used by admin database user for CRUD operations
- Supports creating, updating, and deleting OIDC clients
user_id
: Internal unique identifierusername
: Login identifier, used as OIDC subject (sub
) claim for OBP-API compatibilityfirstname
,lastname
: User's full nameemail
: User's email addressprovider
: Authentication provider, used as OIDC issuer (iss
) claim for OBP-API compatibilityvalidated
: Must be true for authentication
client_id
: Unique client identifierclient_secret
: Client authentication secretclient_name
: Human-readable client nameredirect_uris
: Comma-separated list of allowed redirect URIsgrant_types
: Supported OAuth2 grant typesresponse_types
: Supported OAuth2 response typesscopes
: Available OAuth2 scopestoken_endpoint_auth_method
: Client authentication methodcreated_at
: Client registration timestamppassword_pw
: BCrypt password hashpassword_slt
: Password salt for verification
client_id
: Unique client identifierclient_secret
: Secret for confidential clients (optional)client_name
: Human-readable application nameredirect_uris
: JSON array of allowed callback URLsgrant_types
: Supported OAuth2 grant types (default: authorization_code)scopes
: Allowed access scopes (default: openid, profile, email)token_endpoint_auth_method
: Client authentication method
For compatibility with OBP-API, JWT tokens are generated with specific claim mappings from the v_oidc_users
database view:
sub
(Subject): Contains the user'susername
field fromv_oidc_users
- Source:
v_oidc_users.username
- Purpose: OBP-API uses this to identify the user
- Source:
iss
(Issuer): Contains the user'sprovider
field fromv_oidc_users
- Source:
v_oidc_users.provider
- Purpose: OBP-API uses this to identify the authentication provider
- Source:
- Standard claims: Populated from user data in
v_oidc_users
name
: Combined fromv_oidc_users.firstname
andv_oidc_users.lastname
email
: Fromv_oidc_users.email
email_verified
: Fromv_oidc_users.validated
This ensures that OBP-API can correctly identify users using the sub
field as username and iss
field as provider, exactly as required for proper integration.
The OIDC server accepts tokens with various provider-based issuers, providing flexibility for different authentication providers while maintaining security.
-
Authorization Request:
http://localhost:9000/auth?response_type=code&client_id=test-client&redirect_uri=https://example.com/callback&scope=openid%20profile%20email&state=abc123
-
User Login: Enter valid database user credentials
-
Authorization Code: Redirected to your callback URL with code parameter
-
Token Exchange:
curl -X POST http://localhost:9000/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code&code=YOUR_CODE&redirect_uri=https://example.com/callback&client_id=test-client"
-
UserInfo Request:
curl http://localhost:9000/userinfo \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
The server will test the database connection on startup and log the results.
Run the test suite:
mvn test
The tests demonstrate:
- Discovery document validation
- JWKS endpoint functionality
- Authorization flow with login forms
- Token generation and validation
- UserInfo endpoint with Bearer tokens
- Complete end-to-end OIDC flow
src/main/scala/com/tesobe/oidc/
├── server/ # Main server setup
├── endpoints/ # OIDC endpoint implementations
├── auth/ # Database authentication service
├── tokens/ # JWT token handling
├── models/ # Data models with Circe JSON support
└── config/ # Configuration management
- DatabaseAuthService: PostgreSQL-based user authentication
- JwtService: JWT token generation and validation with RSA256
- CodeService: Authorization code management with expiration
- Endpoints: Individual OIDC endpoint implementations
- OidcServer: Main application with http4s routing
- Doobie: Functional database access with connection pooling
- HikariCP: Connection pool management with proper timeouts
- Automatic Bootstrap: Creates standard OBP ecosystem clients on startup
- Environment Configuration: Fully customizable via environment variables
- Secure Secret Generation: Auto-generates cryptographically secure client secrets
- Update Detection: Intelligently updates clients when configuration changes
- BCrypt: Password verification compatible with OBP-API
- Read-only Access: Uses dedicated
oidc_user
with minimal permissions
To add a new client that gets automatically created during server startup:
-
Edit the ClientBootstrap.scala file:
OBP-OIDC/src/main/scala/com/tesobe/oidc/bootstrap/ClientBootstrap.scala
-
Add your client definition to the
CLIENT_DEFINITIONS
list:ClientDefinition( name = "your-new-client-id", redirect_uris = "http://localhost:PORT/callback,http://localhost:PORT/oauth/callback" )
-
Restart the server - Your new client will be automatically created and its configuration printed to the console for integration with your application.
Example: The existing obp-opey-ii-client
is defined as:
ClientDefinition(
name = "obp-opey-ii-client",
redirect_uris = "http://localhost:5000/callback,http://localhost:5000/oauth/callback"
)
- Pure functions where possible
- Cats Effect IO for side effects
- Immutable data structures
- Monadic error handling with Either types
- Thread-safe database access with connection pooling
- Database user has read-only access to validated users only
- BCrypt password verification with proper salt handling
- Connection pooling with leak detection and timeouts
- SSL/TLS preference for database connections
- Comprehensive logging for security monitoring
- No password storage in memory beyond verification
If you encounter redirect issues where the system redirects to http://localhost:8080/oauth/callback
instead of http://localhost:8080/auth/openid-connect/callback
, you need to update the database record.
Root Cause: The consumer
table stores the client's redirecturl
field, which may have been set incorrectly during initial client creation.
Fix Steps:
-
Run the SQL fix script:
psql -d sandbox -f fix-callback-url.sql
-
Or manually update via SQL:
UPDATE consumer SET redirecturl = 'http://localhost:8080/auth/openid-connect/callback' WHERE consumerid = 'obp-api-client';
-
Verify the fix:
SELECT consumerid, name, redirecturl FROM consumer WHERE consumerid = 'obp-api-client';
Expected Output:
client_id | client_name | redirecturl
--------------+-----------------------+------------------------------------------------
obp-api-client| OBP-API Core Service | http://localhost:8080/auth/openid-connect/callback
This ensures the OAuth authorization flow redirects to the correct OBP-API endpoint.
Problem: OBP-OIDC was unable to verify passwords hashed by OBP-API due to incompatible BCrypt format handling.
Root Cause: OBP-API uses Lift framework's MegaProtoUser which stores BCrypt hashes in a custom format:
- Format:
password_pw = "b;" + BCrypt.hashpw(password, salt).substring(0, 44)
- The "b;" prefix indicates BCrypt format
- Hash is truncated to 44 characters
- Salt is stored separately in
password_slt
field
Solution Implemented:
-
Added jBCrypt dependency (same library used by OBP-API):
<dependency> <groupId>org.mindrot</groupId> <artifactId>jbcrypt</artifactId> <version>0.4</version> </dependency>
-
Updated password verification logic in
DatabaseAuthService.scala
:if (storedHash.startsWith("b;")) { val hashWithoutPrefix = storedHash.substring(2) // Remove "b;" prefix val generatedHash = JBCrypt.hashpw(plainPassword, salt).substring(0, 44) val isMatch = generatedHash == hashWithoutPrefix }
-
Database view compatibility - Uses existing
v_oidc_users
view fields:password_pw
- Contains "b;" + truncated hashpassword_slt
- Contains BCrypt salt
Verification Process:
- Detect "b;" prefix format
- Extract hash without prefix
- Generate hash using
JBCrypt.hashpw(password, salt)
- Truncate to 44 characters
- Compare with stored hash
Testing:
- Use
test-password-verification.scala
to validate implementation - Comprehensive debug logging added for troubleshooting
- Character-by-character comparison for failed attempts
This fix ensures 100% compatibility with OBP-API password verification.
If the server hangs during startup (especially after showing "🚀 Initializing OBP ecosystem OIDC clients..."):
-
Admin Database Issues: The server may be waiting for admin database connection
# Check if admin database user exists and has permissions ./test-admin-db.sh
-
Quick Fix: The server has built-in timeouts (15 seconds) and will continue startup
- Wait up to 30 seconds for automatic recovery
- Look for timeout warnings in logs
- Server will provide manual SQL commands if admin DB unavailable
-
Manual Client Creation: If admin database unavailable, copy SQL from server logs:
INSERT INTO v_oidc_admin_clients (client_id, client_secret, client_name, ...) VALUES ('obp-api-client', 'your-secret', 'OBP-API', ...);
-
Disable Client Bootstrap: Set environment variable to skip:
export OIDC_SKIP_CLIENT_BOOTSTRAP=true
- Verify PostgreSQL is running and accessible
- Check database credentials and permissions:
OIDC_USER_USERNAME
andOIDC_USER_PASSWORD
for read-only accessOIDC_ADMIN_USERNAME
andOIDC_ADMIN_PASSWORD
for client management
- Ensure database views exist:
v_oidc_users
for user authenticationv_oidc_clients
for client validationv_oidc_admin_clients
for client management (optional)
- Review database logs for connection errors
- Verify user exists and is validated in authuser table
- Check password hash format - should start with "b;" for OBP-API compatibility
- Verify jBCrypt library is available (org.mindrot:jbcrypt:0.4)
- Review application logs for detailed password verification debug output
- Use test-password-verification.scala to validate hash generation
- Ensure database view returns expected user data
If authentication succeeds but redirects to wrong URL:
-
Check the
consumer
tableredirecturl
field:SELECT consumerid, redirecturl FROM consumer WHERE consumerid = 'obp-api-client';
-
Should be:
http://localhost:8080/auth/openid-connect/callback
(not/oauth/callback
) -
Fix with:
psql -d sandbox -f fix-callback-url.sql
-
Restart OBP-OIDC service after database changes
For troubleshooting authentication flows, token generation, and other detailed operations, you can enable TRACE level logging:
Normal logging (DEBUG level):
./run-server.sh
TRACE logging enabled:
OIDC_ENABLE_TRACE_LOGGING=true ./run-server.sh
When TRACE logging is enabled, you'll see detailed information about:
- Authorization code validation: Entry points, code lookup, validation results
- Token generation: ID token creation, access token creation, JWT signing
- User authentication: Database queries, password verification steps
- Client operations: Client lookup, validation processes
- Internal state: Memory storage contents, processing steps
Example TRACE output:
validateAndConsumeCode ENTRY - code: 12345678..., clientId: abc123
Found 5 stored codes in memory
Authorization code validation SUCCESS for sub: user123
Generating ID token for user: user123, client: abc123
Setting azp (Authorized Party) claim to: abc123
ID token generated successfully with azp: abc123
- Debugging authentication failures: See exactly where the process fails
- Token generation issues: Track JWT creation and claims
- Integration problems: Understand the complete OIDC flow
- Performance analysis: Identify bottlenecks in the authentication process
- Development: Understand internal workings during feature development
Note: TRACE logging is temporary per session and doesn't modify configuration files. It uses system properties to override the default DEBUG level logging.
This project is licensed under the same terms as the Open Bank Project.
- Pure Functional Programming: Built with Cats Effect IO and immutable data structures
- Modern Scala: Uses http4s, Circe for JSON, and functional error handling
- PostgreSQL Database: Authenticates real users from OBP authuser table via read-only view
- Complete OIDC Support: All essential endpoints for authorization code flow
- Client Management: CRUD operations for OIDC clients via admin database user
- Automatic Client Creation: Auto-creates OBP-API, Portal, Explorer II, and Opey II clients on startup
- JWT Tokens: RS256 signed ID tokens and access tokens
- BCrypt Password Verification: Compatible with OBP-API password hashing
- Integration Tests: Comprehensive test suite demonstrating full OIDC flow
- Language: Scala 2.13 with functional programming principles
- HTTP Framework: http4s with Ember server
- Effect System: Cats Effect IO
- Database: PostgreSQL with Doobie for functional database access
- JSON: Circe for serialization/deserialization
- JWT: Auth0 Java JWT library
- Build Tool: Maven
- Testing: ScalaTest