diff --git a/.fleet/receipt.json b/.fleet/receipt.json new file mode 100644 index 00000000..212afb24 --- /dev/null +++ b/.fleet/receipt.json @@ -0,0 +1,33 @@ +{ + "spec": { + "template_id": "kmt", + "targets": { + "android": { + "ui": [ + "compose" + ] + }, + "ios": { + "ui": [ + "compose" + ] + }, + "desktop": { + "ui": [ + "compose" + ] + }, + "web": { + "ui": [ + "compose" + ] + }, + "server": { + "engine": [ + "ktor" + ] + } + } + }, + "timestamp": "2024-05-31T15:12:19.991061605Z" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9acc12e1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: Run CI +on: + push: + workflow_dispatch: + +jobs: + gradle: + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: adopt-hotspot + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + - name: Grant execute permission for Gradlew (Linux/Mac) + if: runner.os != 'Windows' + run: chmod +x ./gradlew + + - name: Execute Gradle build + run: ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f0e9e76a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*.iml +.kotlin +.gradle +**/build/ +xcuserdata +!src/**/build/ +local.properties +.idea +.DS_Store +captures +.externalNativeBuild +.cxx +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings +/build-tools/ +/platforms/ +/platform-tools/ +/.temp/ diff --git a/.knownPackages b/.knownPackages new file mode 100644 index 00000000..0ad0d522 --- /dev/null +++ b/.knownPackages @@ -0,0 +1 @@ +r©¿FÊ#óÜ–× +
+ Sphereon +
OpenID Federation Monorepo +
+ + +# 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 using 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. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..ec93b697 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.jetbrainsCompose) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.kotlinJvm) apply false + alias(libs.plugins.kotlinMultiplatform) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..05c62c42 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,15 @@ +kotlin.code.style=official + +#Gradle +org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" + +#Android +android.nonTransitiveRClass=true +android.useAndroidX=true + +#Ktor +io.ktor.development=true + +#MPP +kotlin.mpp.androidSourceSetLayoutVersion=2 +kotlin.mpp.enableCInteropCommonization=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..4559b0da --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,44 @@ +[versions] +agp = "8.2.0" +android-compileSdk = "34" +android-minSdk = "24" +android-targetSdk = "34" +androidx-activityCompose = "1.9.0" +androidx-appcompat = "1.6.1" +androidx-constraintlayout = "2.1.4" +androidx-core-ktx = "1.13.1" +androidx-espresso-core = "3.5.1" +androidx-material = "1.12.0" +androidx-test-junit = "1.1.5" +compose-plugin = "1.6.10" +junit = "4.13.2" +kotlin = "2.0.0" +kotlinxSerializationJson = "1.7.0-RC" +ktor = "2.3.11" +logback = "1.5.6" + +[libraries] +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } +androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } +androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } +ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" } +ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +ktor = { id = "io.ktor.plugin", version.ref = "ktor" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7f93135c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..b82aa23a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/licenses/android-googletv-license b/licenses/android-googletv-license new file mode 100644 index 00000000..07d43f0b --- /dev/null +++ b/licenses/android-googletv-license @@ -0,0 +1,2 @@ + +601085b94cd77f0b54ff86406957099ebe79c4d6 \ No newline at end of file diff --git a/licenses/android-sdk-arm-dbt-license b/licenses/android-sdk-arm-dbt-license new file mode 100644 index 00000000..0dc9cc07 --- /dev/null +++ b/licenses/android-sdk-arm-dbt-license @@ -0,0 +1,2 @@ + +859f317696f67ef3d7f30a50a5560e7834b43903 \ No newline at end of file diff --git a/licenses/android-sdk-license b/licenses/android-sdk-license new file mode 100644 index 00000000..b8d7962a --- /dev/null +++ b/licenses/android-sdk-license @@ -0,0 +1,2 @@ + +24333f8a63b6825ea9c5514f83c2829b004d1fee \ No newline at end of file diff --git a/licenses/android-sdk-preview-license b/licenses/android-sdk-preview-license new file mode 100644 index 00000000..da4552d2 --- /dev/null +++ b/licenses/android-sdk-preview-license @@ -0,0 +1,2 @@ + +84831b9409646a918e30573bab4c9c91346d8abd \ No newline at end of file diff --git a/licenses/google-gdk-license b/licenses/google-gdk-license new file mode 100644 index 00000000..db3b42fd --- /dev/null +++ b/licenses/google-gdk-license @@ -0,0 +1,2 @@ + +33b6a2b64607f11b759f320ef9dff4ae5c47d97a \ No newline at end of file diff --git a/licenses/intel-android-extra-license b/licenses/intel-android-extra-license new file mode 100644 index 00000000..f82e65b6 --- /dev/null +++ b/licenses/intel-android-extra-license @@ -0,0 +1,2 @@ + +d975f751698a77b662f1254ddbeed3901e976f5a \ No newline at end of file diff --git a/licenses/mips-android-sysimage-license b/licenses/mips-android-sysimage-license new file mode 100644 index 00000000..8f4f164e --- /dev/null +++ b/licenses/mips-android-sysimage-license @@ -0,0 +1,2 @@ + +e9acab5b5fbb560a72cfaecce8946896ff6aab9d \ No newline at end of file diff --git a/local.properties.example b/local.properties.example new file mode 100644 index 00000000..bcf2a894 --- /dev/null +++ b/local.properties.example @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. + +sdk.dir= diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts new file mode 100644 index 00000000..2bc918dd --- /dev/null +++ b/modules/openid-federation-common/build.gradle.kts @@ -0,0 +1,139 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + kotlin("plugin.serialization") version "2.0.0" +} + +kotlin { + @OptIn(ExperimentalWasmDsl::class) + + js { + browser { + commonWebpackConfig { + devServer = KotlinWebpackConfig.DevServer().apply { + port = 8083 + } + } + } + nodejs { + testTask { + useMocha { + timeout = "5000" + } + } + } + } + + // wasmJs is not available yet for ktor until v3.x is released which is still in alpha + + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + jvm() + + sourceSets { + val commonMain by getting { + dependencies { + implementation("io.ktor:ktor-client-core:2.3.11") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmMain by getting { + dependencies { + implementation("io.ktor:ktor-client-cio:2.3.11") + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + + val androidMain by getting { + dependencies { + implementation("io.ktor:ktor-client-okhttp:2.3.11") + } + } + val androidUnitTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + + val iosMain by creating { + dependsOn(commonMain) + dependencies { + implementation("io.ktor:ktor-client-ios:2.3.11") + } + } + val iosX64Main by getting { + dependsOn(iosMain) + } + val iosArm64Main by getting { + dependsOn(iosMain) + } + val iosSimulatorArm64Main by getting { + dependsOn(iosMain) + } + + val iosTest by creating { + dependsOn(commonTest) + dependencies { + implementation(kotlin("test")) + } + } + + val jsMain by getting { + dependencies { + implementation("io.ktor:ktor-client-js:2.3.11") + } + } + + val jsTest by getting { + dependsOn(commonTest) + dependencies { + implementation(kotlin("test-js")) + implementation(kotlin("test-annotations-common")) + } + } + } +} + +tasks.register("printSdkLocation") { + doLast { + println("Android SDK Location: ${android.sdkDirectory}") + } +} + +android { + namespace = "com.sphereon.oid.fed.common" + compileSdk = libs.versions.android.compileSdk.get().toInt() + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } +} + diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt new file mode 100644 index 00000000..2a97e8eb --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt @@ -0,0 +1,144 @@ +package com.sphereon.oid.fed.common.mime + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlin.js.JsExport + +private val qpAllowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~".toSet() + +/** + * URL encode a String. + * Converts characters not allowed in URL query parameters to their percent-encoded equivalents. + * input an input string + * @return URL encoded String + */ +@JsExport +fun urlEncodeValue(input: String): String { + return buildString { + input.forEach { char -> + if (char in qpAllowedChars) { + append(char) + } else { + append('%') + append(char.code.toString(16).uppercase().padStart(2, '0')) + } + } + } +} + +/** + * Extension function to URL encode a String. + * Converts characters not allowed in URL query parameters to their percent-encoded equivalents. + * + * @return URL encoded String + */ +fun String.toUrlEncodedValue(): String { + return urlEncodeValue(this) +} + +/** + * Extension function to URL encode a JsonElement. + * Converts the JsonElement to a JSON String and then URL encodes it. + * + * @return URL encoded String representation of the JsonElement + */ +fun JsonElement.toUrlEncodedValue(): String { + return this.toString().toUrlEncodedValue() +} + +/** + * Inline function to URL encode any serializable object. + * Converts the object to a JSON string and then URL encodes it. + * + * @return URL encoded JSON String representation of the object + */ +inline fun T.toUrlEncodedJsonValue(): String { + return Json.encodeToString(this).toUrlEncodedValue() +} + +/** + * Function to URL encode any serializable object using a provided serializer. + * Converts the object to a JSON string using the serializer and then URL encodes it. + * + * @param serializer The serializer to use for converting the object to a JSON string + * @return URL encoded JSON String representation of the object + */ +fun T.toUrlEncodedJsonValue(serializer: KSerializer): String { + return Json.encodeToString(serializer, this).toUrlEncodedValue() +} + +/** + * Extension function to decode a URL encoded String. + * Converts percent-encoded characters back to their original form. + * + * input An URL encoded input string + * @return Decoded String + */ +@JsExport +fun urlDecodeValue(input: String): String { + return buildString { + var i = 0 + while (i < input.length) { + when (val char = input[i]) { + '%' -> { + if (i + 2 >= input.length) { + throw IllegalArgumentException("Incomplete percent encoding at position $i") + } + append(input.substring(i + 1, i + 3).toInt(16).toChar()) + i += 3 + } + + else -> { + append(char) + i++ + } + } + } + } +} + +/** + * Extension function to decode a URL encoded String. + * Converts percent-encoded characters back to their original form. + * + * @return Decoded String + */ +fun String.fromUrlEncodedValue(): String { + return urlDecodeValue(this) +} + +/** + * Extension function to decode a URL encoded JSON String to a JsonElement. + * Decodes the URL encoded String and parses it to a JsonElement. + * + * @return Decoded JsonElement + */ +fun String.fromUrlEncodedJsonValueToJsonElement(): JsonElement { + val decodedString = this.fromUrlEncodedValue() + return Json.parseToJsonElement(decodedString) +} + +/** + * Inline function to decode a URL encoded JSON String to an object of type T. + * Decodes the URL encoded String and deserializes it to an object of type T. + * + * @return Deserialized object of type T + */ +inline fun String.fromUrlEncodedJsonValue(): T { + val decodedString = this.fromUrlEncodedValue() + return Json.decodeFromString(decodedString) +} + +/** + * Function to decode a URL encoded JSON String to an object of type T using a provided serializer. + * Decodes the URL encoded String and deserializes it to an object of type T using the serializer. + * + * @param serializer The serializer to use for deserializing the JSON string + * @return Deserialized object of type T + */ +fun String.fromUrlEncodedJsonValue(serializer: KSerializer): T { + val decodedString = this.fromUrlEncodedValue() + return Json.decodeFromString(serializer, decodedString) +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderKtor.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderKtor.kt new file mode 100644 index 00000000..f58b41ed --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderKtor.kt @@ -0,0 +1,92 @@ +package com.sphereon.oid.fed.common.mime + +import io.ktor.http.* +import kotlinx.serialization.json.JsonElement + +/** + * Extension function for ParametersBuilder to append a URL encoded value. + * Encodes the given data string and appends it to the ParametersBuilder with the specified name. + * + * @param name The name of the parameter + * @param data The data to be URL encoded and appended + * @return The ParametersBuilder with the appended value + * @throws IllegalArgumentException if the name or data is empty + */ +fun ParametersBuilder.appendUrlEncodedValue(name: String, data: String): ParametersBuilder { + require(name.isNotEmpty()) { "Parameter name cannot be empty" } + require(data.isNotEmpty()) { "data cannot be empty" } + + this.append(name, data.toUrlEncodedValue()) + return this +} + +/** + * Extension function for ParametersBuilder to append a URL encoded JsonElement. + * Converts the JsonElement to a string, URL encodes it, and appends it to the ParametersBuilder with the specified name. + * + * @param name The name of the parameter + * @param jsonElement The JsonElement to be URL encoded and appended + * @return The ParametersBuilder with the appended value + * @throws IllegalArgumentException if the name is empty + */ +fun ParametersBuilder.appendUrlEncodedValue(name: String, jsonElement: JsonElement): ParametersBuilder { + require(name.isNotEmpty()) { "Parameter name cannot be empty" } + + this.append(name, jsonElement.toString().toUrlEncodedValue()) + return this +} + +/** + * Extension function for Parameters to decode URL encoded values. + * Decodes all URL encoded values in the Parameters and returns them as a Map. + * + * @return A Map containing the decoded parameter names and values + */ +fun Parameters.fromUrlEncodedValues(): Map { + return this.entries().mapNotNull { + val value = it.value.firstOrNull()?.fromUrlEncodedValue() + if (value != null) it.key to value else null + }.toMap() +} + +/** + * Extension function for Parameters to decode URL encoded JSON values to JsonElements. + * Decodes all URL encoded JSON values in the Parameters and returns them as a Map. + * + * @return A Map containing the decoded parameter names and JsonElements + */ +fun Parameters.fromUrlEncodedJsonValuesToJsonElements(): Map { + return this.entries().mapNotNull { + val value = it.value.firstOrNull()?.fromUrlEncodedJsonValueToJsonElement() + if (value != null) it.key to value else null + }.toMap() +} + +/** + * Extension function for Parameters to get and decode a URL encoded value. + * Retrieves the value for the specified name, decodes it, and returns the result. + * + * @param name The name of the parameter to retrieve + * @return The decoded value + * @throws IllegalArgumentException if the name is empty + * @throws NoSuchElementException if no value is found for the specified name + */ +fun Parameters.getUrlEncodedValue(name: String): String { + require(name.isNotEmpty()) { "Parameter name cannot be empty" } + return this[name]?.fromUrlEncodedValue() ?: throw NoSuchElementException("No value found for key: $name") +} + +/** + * Extension function for Parameters to get and decode a URL encoded JSON value to a JsonElement. + * Retrieves the value for the specified name, decodes it to a JsonElement, and returns the result. + * + * @param name The name of the parameter to retrieve + * @return The decoded JsonElement + * @throws IllegalArgumentException if the name is empty + * @throws NoSuchElementException if no value is found for the specified name + */ +fun Parameters.getUrlEncodedJsonValueToJsonElement(name: String): JsonElement { + require(name.isNotEmpty()) { "Parameter name cannot be empty" } + return this[name]?.fromUrlEncodedJsonValueToJsonElement() + ?: throw NoSuchElementException("No value found for key: $name") +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTest.kt new file mode 100644 index 00000000..97223838 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTest.kt @@ -0,0 +1,87 @@ +package com.sphereon.oid.fed.common.mime + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test +import kotlin.test.assertEquals + + +@Serializable +data class PreAuthorizedCode(val pre_authorized_code: String) + +@Serializable +data class Grants(val urn_ietf_params_oauth_grant_type_pre_authorized_code: PreAuthorizedCode) + +@Serializable +data class TestData( + val grants: Grants, + val credential_configuration_ids: List, + val credential_issuer: String +) + +class JsonUrlEncoderTest { + private val originalJson = + """{"grants":{"urn:ietf:params:oauth:grant-type:pre-authorized_code":{"pre-authorized_code":"a"}},"credential_configuration_ids":["DummyCredential"],"credential_issuer":"https://agent.issuer.dummy.com"}""" + private val encodedJson = + "%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22a%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22DummyCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fagent.issuer.dummy.com%22%7D" + private val encodedJsonFromObject = // In objects, we can have colons in the field name + "%7B%22grants%22%3A%7B%22urn_ietf_params_oauth_grant_type_pre_authorized_code%22%3A%7B%22pre_authorized_code%22%3A%22a%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22DummyCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fagent.issuer.dummy.com%22%7D" + + private val testData = TestData( + grants = Grants(PreAuthorizedCode("a")), + credential_configuration_ids = listOf("DummyCredential"), + credential_issuer = "https://agent.issuer.dummy.com" + ) + + @Test + fun testStringToUrlEncodedValue() { + val result = originalJson.toUrlEncodedValue() + assertEquals(encodedJson, result) + } + + @Test + fun testJsonElementToUrlEncodedValue() { + val original: JsonElement = Json.parseToJsonElement(originalJson) + val result = original.toUrlEncodedValue() + assertEquals(encodedJson, result) + } + + @Test + fun testObjectToUrlEncodedJsonValue() { + val result = testData.toUrlEncodedJsonValue() + assertEquals(encodedJsonFromObject, result) + } + + @Test + fun testKSerializerToUrlEncodedJsonValue() { + val serializer = TestData.serializer() + val result = testData.toUrlEncodedJsonValue(serializer) + assertEquals(encodedJsonFromObject, result) + } + + @Test + fun testFromUrlEncodedValue() { + val result = encodedJson.fromUrlEncodedValue() + assertEquals(originalJson, result) + } + + @Test + fun testFromUrlEncodedValueToJsonElement() { + val result = encodedJson.fromUrlEncodedJsonValueToJsonElement() + assertEquals(Json.parseToJsonElement(originalJson), result) + } + + @Test + fun testFromUrlEncodedJsonValue() { + val result: TestData = encodedJsonFromObject.fromUrlEncodedJsonValue() + assertEquals(testData, result) + } + + @Test + fun testFromUrlEncodedJsonValueWithSerializer() { + val serializer = TestData.serializer() + val result: TestData = encodedJsonFromObject.fromUrlEncodedJsonValue(serializer) + assertEquals(testData, result) + } +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTestKtor.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTestKtor.kt new file mode 100644 index 00000000..da6cc437 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTestKtor.kt @@ -0,0 +1,86 @@ +package com.sphereon.oid.fed.common.mime + +import io.ktor.http.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class JsonUrlEncoderTestKtor { + private val originalJson = + """{"grants":{"urn:ietf:params:oauth:grant-type:pre-authorized_code":{"pre-authorized_code":"a"}},"credential_configuration_ids":["DummyCredential"],"credential_issuer":"https://agent.issuer.dummy.com"}""" + private val encodedJson = + "%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22a%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22DummyCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fagent.issuer.dummy.com%22%7D" + + + @Test + fun testAppendUrlEncodedValueWithString() { + val parametersBuilder = ParametersBuilder() + parametersBuilder.appendUrlEncodedValue("test", originalJson) + val result = parametersBuilder.build()["test"] + assertEquals(encodedJson, result) + } + + @Test + fun testAppendUrlEncodedValueWithJsonElement() { + val jsonElement: JsonElement = Json.parseToJsonElement(originalJson) + val parametersBuilder = ParametersBuilder() + parametersBuilder.appendUrlEncodedValue("test", jsonElement) + val result = parametersBuilder.build()["test"] + assertEquals(encodedJson, result) + } + + @Test + fun testFromUrlEncodedValues() { + val parameters = ParametersBuilder().apply { + append("test", encodedJson) + }.build() + val result = parameters.fromUrlEncodedValues() + assertEquals(mapOf("test" to originalJson), result) + } + + @Test + fun testFromUrlEncodedJsonValuesToJsonElements() { + val parameters = ParametersBuilder().apply { + append("test", encodedJson) + }.build() + val result = parameters.fromUrlEncodedJsonValuesToJsonElements() + assertEquals(mapOf("test" to Json.parseToJsonElement(originalJson)), result) + } + + @Test + fun testGetUrlEncodedValue() { + val parameters = ParametersBuilder().apply { + append("test", encodedJson) + }.build() + val result = parameters.getUrlEncodedValue("test") + assertEquals(originalJson, result) + } + + @Test + fun testGetUrlEncodedJsonValueToJsonElement() { + val parameters = ParametersBuilder().apply { + append("test", encodedJson) + }.build() + val result = parameters.getUrlEncodedJsonValueToJsonElement("test") + assertEquals(Json.parseToJsonElement(originalJson), result) + } + + @Test + fun testGetUrlEncodedValueThrowsException() { + val parameters = ParametersBuilder().build() + assertFailsWith { + parameters.getUrlEncodedValue("test") + } + } + + @Test + fun testGetUrlEncodedJsonValueToJsonElementThrowsException() { + val parameters = ParametersBuilder().build() + assertFailsWith { + parameters.getUrlEncodedJsonValueToJsonElement("test") + } + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..fbf00ad3 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,31 @@ +rootProject.name = "kotlin-mp-genesis" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +pluginManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} + +include(":modules:openid-federation-common")