Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/OIDF-51 - Implement Persistence Module #21

Merged
merged 11 commits into from
Aug 6, 2024
Merged
Changes from 8 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
8 changes: 4 additions & 4 deletions .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db
DATASOURCE_USER=openid-federation-db-user
DATASOURCE_PASSWORD=openid-federation-db-password
DATASOURCE_DB=openid-federation-db
DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db
DATASOURCE_USER=openid-federation-db-user
DATASOURCE_PASSWORD=openid-federation-db-password
DATASOURCE_DB=openid-federation-db
142 changes: 71 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,71 @@
<h1 align="center">
<br>
<a href="https://www.sphereon.com"><img src="https://sphereon.com/content/themes/sphereon/assets/img/logo.svg" alt="Sphereon" width="400"></a>
<br>OpenID Federation Monorepo
<br>
</h1>

# Background

OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities to participate in federations, ensuring trust and security across different organizations and systems.

In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and capabilities in a standardized manner, enabling seamless integration and interoperability within federations.

## Key Concepts

- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a federation policy.
- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation relationships.
- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of entity statements, leading back to a trusted authority.
- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for federation management.

## Core Components

- **Federation Operator**: The central authority in a federation that manages policy and trust chain verification.
- **Identity Providers (IdPs)**: Entities that authenticate users and provide identity assertions to relying parties.
- **Relying Parties (RPs)**: Entities that rely on identity assertions provided by IdPs to offer services to users.

## Technical Features

- **JSON Web Tokens (JWT)**: Used for creating verifiable entity statements and security assertions.
- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality.

## Operational Model

- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on real-time verification of entity statements.
- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically adjusted according to real-time interactions and policy evaluations.
- **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved.

# Data Structure

## Entity Statement Overview

### 1. Definition
- An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations.
- **Entity Configuration**: An Entity Statement about itself.
- **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity.

### 2. Requirements and Structure
- **Type**: JWT must be explicitly typed as `entity-statement+jwt`.
- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256).
- **Key ID (kid)**: The header must include the Key ID of the signing key.

### 3. Claims in an Entity Statement
- **iss (Issuer)**: Entity Identifier of the issuer.
- **sub (Subject)**: Entity Identifier of the subject.
- **iat (Issued At)**: Time the statement was issued.
- **exp (Expiration Time)**: Time after which the statement is no longer valid.
- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit Registration.
- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate Statements.
- **metadata** (Optional): Represents the Entity’s Types and metadata.
- **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates.
- **constraints** (Optional): Defines Trust Chain constraints.
- **crit** (Optional): Specifies critical claims that must be understood and processed.
- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and processed.
- **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark.
- **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks.
- **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities.
- **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer.

### 4. Usage and Flexibility
- Entity Statements can include additional claims as required by applications and protocols.
- Metadata in Subordinate Statements overrides that in the Entity’s own configuration.
<h1 align="center">
<br>
<a href="https://www.sphereon.com"><img src="https://sphereon.com/content/themes/sphereon/assets/img/logo.svg" alt="Sphereon" width="400"></a>
<br>OpenID Federation Monorepo
<br>
</h1>

# Background

OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities to participate in federations, ensuring trust and security across different organizations and systems.

In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and capabilities in a standardized manner, enabling seamless integration and interoperability within federations.

## Key Concepts

- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a federation policy.
- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation relationships.
- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of entity statements, leading back to a trusted authority.
- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for federation management.

## Core Components

- **Federation Operator**: The central authority in a federation that manages policy and trust chain verification.
- **Identity Providers (IdPs)**: Entities that authenticate users and provide identity assertions to relying parties.
- **Relying Parties (RPs)**: Entities that rely on identity assertions provided by IdPs to offer services to users.

## Technical Features

- **JSON Web Tokens (JWT)**: Used for creating verifiable entity statements and security assertions.
- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality.

## Operational Model

- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on real-time verification of entity statements.
- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically adjusted according to real-time interactions and policy evaluations.
- **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved.

# Data Structure

## Entity Statement Overview

### 1. Definition
- An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations.
- **Entity Configuration**: An Entity Statement about itself.
- **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity.

### 2. Requirements and Structure
- **Type**: JWT must be explicitly typed as `entity-statement+jwt`.
- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256).
- **Key ID (kid)**: The header must include the Key ID of the signing key.

### 3. Claims in an Entity Statement
- **iss (Issuer)**: Entity Identifier of the issuer.
- **sub (Subject)**: Entity Identifier of the subject.
- **iat (Issued At)**: Time the statement was issued.
- **exp (Expiration Time)**: Time after which the statement is no longer valid.
- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit Registration.
- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate Statements.
- **metadata** (Optional): Represents the Entity’s Types and metadata.
- **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates.
- **constraints** (Optional): Defines Trust Chain constraints.
- **crit** (Optional): Specifies critical claims that must be understood and processed.
- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and processed.
- **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark.
- **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks.
- **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities.
- **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer.

### 4. Usage and Flexibility
- Entity Statements can include additional claims as required by applications and protocols.
- Metadata in Subordinate Statements overrides that in the Entity’s own configuration.
3,102 changes: 3,102 additions & 0 deletions kotlin-js-store/yarn.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions modules/admin-server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -16,7 +16,9 @@ java {
}

dependencies {
api(projects.modules.openapi)
api(projects.modules.openidFederationCommon)
api(projects.modules.persistence)
implementation(libs.springboot.actuator)
implementation(libs.springboot.web)
implementation(libs.springboot.data.jdbc)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.sphereon.oid.fed.server.admin

class Constants {
companion object {
const val ACCOUNT_ALREADY_EXISTS = "Account already exists"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.sphereon.oid.fed.server.admin.controllers

import com.sphereon.oid.fed.openapi.models.AccountDTO
import com.sphereon.oid.fed.openapi.models.CreateAccountDTO
import com.sphereon.oid.fed.server.admin.services.AccountService
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/account")
class AccountController {
private val accountService = AccountService()

@GetMapping
fun getAccounts(): List<AccountDTO> {
return accountService.findAll()
}

@PostMapping
fun createAccount(@RequestBody account: CreateAccountDTO): AccountDTO {
return accountService.create(account)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.sphereon.oid.fed.server.admin.services

import com.sphereon.oid.fed.openapi.models.AccountDTO
import com.sphereon.oid.fed.openapi.models.CreateAccountDTO
import com.sphereon.oid.fed.persistence.Persistence
import com.sphereon.oid.fed.persistence.extensions.toDTO
import com.sphereon.oid.fed.server.admin.Constants


class AccountService {
private val accountRepository = Persistence.accountRepository

fun create(account: CreateAccountDTO): AccountDTO {
val accountAlreadyExists = accountRepository.findByUsername(account.username) != null

if (accountAlreadyExists) {
throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS)
}

return accountRepository.create(account).executeAsOne().toDTO()
}

fun findAll(): List<AccountDTO> {
return accountRepository.findAll().map { it.toDTO() }
}
}
3 changes: 2 additions & 1 deletion modules/openapi/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -35,7 +35,8 @@ kotlin {
filter { line: String ->
line.replace(
"kotlin.collections.Map<kotlin.String, kotlin.Any>",
"kotlinx.serialization.json.JsonObject")
"kotlinx.serialization.json.JsonObject"
)
}
}

2,578 changes: 2,380 additions & 198 deletions modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions modules/persistence/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
plugins {
kotlin("multiplatform") version "2.0.0"
id("app.cash.sqldelight") version "2.0.2"
}

group = "com.sphereon.oid.fed.persistence"
version = "0.1.0"

repositories {
google()
mavenCentral()
mavenLocal()
}

sqldelight {
databases {
create("Database") {
packageName = "com.sphereon.oid.fed.persistence"
dialect("app.cash.sqldelight:postgresql-dialect:2.0.2")
schemaOutputDirectory = file("src/commonMain/resources/db/migration")
migrationOutputDirectory = file("src/commonMain/resources/db/migration")
deriveSchemaFromMigrations = true
migrationOutputFileFormat = ".sql"
srcDirs.from(
"src/commonMain/sqldelight"
)
}
}
}

kotlin {
jvm()

sourceSets {
commonMain {
dependencies {
implementation(projects.modules.openapi)
}
}

jvmMain {
dependencies {
implementation("app.cash.sqldelight:jdbc-driver:2.0.2")
implementation("com.zaxxer:HikariCP:5.1.0")
implementation("org.postgresql:postgresql:42.7.3")
}
}
}
}
Binary file not shown.
6 changes: 6 additions & 0 deletions modules/persistence/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#Thu Jul 25 12:58:25 CEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.sphereon.oid.fed.persistence

class Constants {
companion object {
const val DATASOURCE_URL = "DATASOURCE_URL"
const val DATASOURCE_USER = "DATASOURCE_USER"
const val DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD"
const val SQLITE_IS_NOT_SUPPORTED_IN_JVM = "SQLite is not supported in JVM"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.sphereon.oid.fed.persistence

import com.sphereon.oid.fed.persistence.repositories.AccountRepository

expect object Persistence {
val accountRepository: AccountRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sphereon.oid.fed.persistence.database

import app.cash.sqldelight.db.SqlDriver

expect class PlatformSqlDriver {
fun createPostgresDriver(url: String, username: String, password: String): SqlDriver
fun createSqliteDriver(path: String): SqlDriver
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.sphereon.oid.fed.persistence.extensions

import com.sphereon.oid.fed.openapi.models.AccountDTO
import com.sphereon.oif.fed.persistence.models.Account

fun Account.toDTO(): AccountDTO {
return AccountDTO(
username = this.username
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.sphereon.oid.fed.persistence.repositories

import app.cash.sqldelight.ExecutableQuery
import com.sphereon.oid.fed.openapi.models.CreateAccountDTO
import com.sphereon.oif.fed.persistence.models.Account
import com.sphereon.oif.fed.persistence.models.AccountQueries

class AccountRepository(accountQueries: AccountQueries) {
private val accountQueries = accountQueries

fun findById(id: Int): Account? {
return accountQueries.findById(id).executeAsOneOrNull()
}

fun findByUsername(username: String): Account? {
return accountQueries.findByUsername(username).executeAsOneOrNull()
}

fun create(account: CreateAccountDTO): ExecutableQuery<Account> {
return accountQueries.create(username = account.username)
}

fun findAll(): List<Account> {
return accountQueries.findAll().executeAsList()
}

fun delete(id: Int) {
return accountQueries.delete(id)
}

fun update(id: Int, account: Account) {
return accountQueries.update(account.username, id)
}
}
11 changes: 11 additions & 0 deletions modules/persistence/src/commonMain/resources/db/migration/1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE account (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);

CREATE INDEX account_username_index ON account (username);

INSERT INTO account (username) VALUES ('root');
zoemaas marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE account (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);

CREATE INDEX account_username_index ON account (username);

INSERT INTO account (username) VALUES ('root');

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
findAll:
SELECT * FROM account;

create:
INSERT INTO account (username) VALUES (?) RETURNING *;

delete:
UPDATE account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?;

findByUsername:
SELECT * FROM account WHERE username = ?;

findById:
SELECT * FROM account WHERE id = ?;

update:
UPDATE account SET username = ? WHERE id = ?;

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.sphereon.oid.fed.persistence

import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlCursor
import app.cash.sqldelight.db.SqlDriver
import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver
import com.sphereon.oid.fed.persistence.repositories.AccountRepository

actual object Persistence {
actual val accountRepository: AccountRepository

init {
val driver = getDriver()
runMigrations(driver)

val database = Database(driver)
accountRepository = AccountRepository(database.accountQueries)
}

private fun getDriver(): SqlDriver {
return PlatformSqlDriver().createPostgresDriver(
System.getenv(Constants.DATASOURCE_URL),
System.getenv(Constants.DATASOURCE_USER),
System.getenv(Constants.DATASOURCE_PASSWORD)
)
}

private fun runMigrations(driver: SqlDriver) {
setupSchemaVersioningTable(driver)

val currentVersion = getCurrentDatabaseVersion(driver)
val newVersion = Database.Schema.version

if (currentVersion < newVersion) {
Database.Schema.migrate(driver, currentVersion, newVersion)
updateDatabaseVersion(driver, newVersion)
}
}

private fun setupSchemaVersioningTable(driver: SqlDriver) {
driver.execute(null, "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)", 0)
}

private fun getCurrentDatabaseVersion(driver: SqlDriver): Long {
val versionQuery = "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1"

val version = driver.executeQuery(null, versionQuery, parameters = 0, mapper = { cursor: SqlCursor ->
QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else null)
})

return version.value ?: 0
}

private fun updateDatabaseVersion(driver: SqlDriver, newVersion: Long) {
val updateQuery = "INSERT INTO schema_version (version) VALUES (?)"
driver.execute(null, updateQuery, 1) {
bindLong(0, newVersion)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.sphereon.oid.fed.persistence.database

import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.asJdbcDriver
import com.sphereon.oid.fed.persistence.Constants
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource

actual class PlatformSqlDriver {
actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver {
val config = HikariConfig()
config.jdbcUrl = url
config.username = username
config.password = password

val dataSource = HikariDataSource(config)
return dataSource.asJdbcDriver()
}

actual fun createSqliteDriver(path: String): SqlDriver {
throw UnsupportedOperationException(Constants.SQLITE_IS_NOT_SUPPORTED_IN_JVM)
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -45,3 +45,4 @@ dependencyResolutionManagement {
include(":modules:openid-federation-common")
include(":modules:admin-server")
include(":modules:openapi")
include(":modules:persistence")