diff --git a/.github/workflows/actions/analyze/action.yml b/.github/workflows/actions/analyze/action.yml new file mode 100644 index 00000000..53ba0eb6 --- /dev/null +++ b/.github/workflows/actions/analyze/action.yml @@ -0,0 +1,10 @@ +name: Code analyze +description: Analyze + +runs: + using: "composite" + steps: + - uses: ./.github/workflows/actions/prepare + - name: Library code Analysis + shell: bash + run: ./gradlew detekt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ce29484..6beea49b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,15 +18,14 @@ permissions: checks: write # required for test-reporter jobs: - build: + analyze: runs-on: macos-15 steps: - uses: actions/checkout@v4 - - uses: ./.github/workflows/actions/build - - uses: ./.github/workflows/actions/doc + - uses: ./.github/workflows/actions/analyze test: - needs: [ build ] + needs: [ analyze ] runs-on: macos-15 permissions: id-token: write @@ -37,10 +36,18 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/workflows/actions/test + build: + needs: [ test ] + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/workflows/actions/build + - uses: ./.github/workflows/actions/doc + create-swift-package: - needs: [ build, test] + needs: [ build, test ] if: github.ref == 'refs/heads/main' - runs-on: macos-15 + runs-on: macos-13 steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/actions/createSwiftPackage @@ -48,7 +55,7 @@ jobs: version: "main" deploy-swift-package-snapshot: - needs: [create-swift-package] + needs: [ create-swift-package ] if: github.ref == 'refs/heads/main' runs-on: macos-15 steps: @@ -59,7 +66,7 @@ jobs: token: ${{ secrets.PAT }} deploy-snapshot: - needs: [ test ] + needs: [ build ] if: github.ref == 'refs/heads/main' runs-on: macos-15 diff --git a/.gitignore b/.gitignore index 9fd27739..68e0a229 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,21 @@ -.idea +*.iml +.kotlin .gradle -build -.hprof -**/.DS_Store -*.hprof +**/build/ +xcuserdata +!src/**/build/ local.properties -.kotlin \ No newline at end of file +.idea +.DS_Store +captures +.externalNativeBuild +.cxx +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings +node_modules/ +xcuserdata/ +kotlin-js-store/ diff --git a/README.md b/README.md index 90b32168..eac0d75f 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ val tokens = flow.getAccessToken() Perform refresh or endSession: ```kotlin -tokens.refresh_token?.let { client.refreshToken(refreshToken = it) } -tokens.id_token?.let { client.endSession(idToken = it) } +tokens.refreshToken?.let { client.refreshToken(refreshToken = it) } +tokens.idToken?.let { client.endSession(idToken = it) } ``` # Token Store (experimental) @@ -183,14 +183,14 @@ If you have configured a ```postLogoutRedirectUri``` and want to perform a Logou you can use the endSession flow: ```kotlin val flow = authFlowFactory.createEndSessionFlow(client) -tokens.id_token?.let { flow.endSession(it) } +tokens.idToken?.let { flow.endSession(it) } ``` That way, browser cookies should be cleared so the next time a client wants to login, it get's prompted for username and password again. # JWT Parsing We provide simple JWT parsing (without any validation): ```kotlin -val jwt = tokens.id_token?.let { Jwt.parse(it) } +val jwt = tokens.idToken?.let { Jwt.parse(it) } println(jwt?.payload?.aud) // print audience println(jwt?.payload?.iss) // print issuer println(jwt?.payload?.additionalClaims?.get("email")) // get claim diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index a4cd9a4c..8e595523 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { compileOnly(libs.nexusPublish.gradlePlugin) compileOnly(libs.multiplatform.swiftpackage.gradlePlugin) compileOnly(libs.dokka.gradlePlugin) + implementation(libs.detekt.gradle) // https://github.com/gradle/gradle/issues/15383 implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) @@ -60,5 +61,9 @@ gradlePlugin { id = "org.publicvalue.convention.multiplatformSwiftPackage" implementationClass = "org.publicvalue.convention.MultiplatformSwiftPackageConventionPlugin" } + register("customDetekt") { + id = "org.publicvalue.convention.detekt" + implementationClass = "org.publicvalue.convention.DetektPlugin" + } } } \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/AndroidApplicationConventionPlugin.kt index 90152aee..5a47bce2 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/AndroidApplicationConventionPlugin.kt @@ -6,7 +6,8 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.publicvalue.convention.config.configureKotlinAndroid -class AndroidApplicationConventionPlugin : Plugin { +@Suppress("unused") +internal class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { @@ -18,4 +19,4 @@ class AndroidApplicationConventionPlugin : Plugin { } } } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/AndroidLibraryConventionPlugin.kt index 862691ce..44c2caf7 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/AndroidLibraryConventionPlugin.kt @@ -1,12 +1,13 @@ package org.publicvalue.convention import com.android.build.gradle.LibraryExtension -import org.publicvalue.convention.config.configureKotlinAndroid import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure +import org.publicvalue.convention.config.configureKotlinAndroid -class AndroidLibraryConventionPlugin : Plugin { +@Suppress("unused") +internal class AndroidLibraryConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { @@ -19,4 +20,4 @@ class AndroidLibraryConventionPlugin : Plugin { } } } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/ComposeMultiplatformConventionPlugin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/ComposeMultiplatformConventionPlugin.kt index 7d252e58..2e62ef67 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/ComposeMultiplatformConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/ComposeMultiplatformConventionPlugin.kt @@ -3,9 +3,10 @@ package org.publicvalue.convention import org.gradle.api.Plugin import org.gradle.api.Project -class ComposeMultiplatformConventionPlugin : Plugin { +@Suppress("unused") +internal class ComposeMultiplatformConventionPlugin : Plugin { override fun apply(target: Project) = with(target) { pluginManager.apply("org.jetbrains.compose") pluginManager.apply("org.jetbrains.kotlin.plugin.compose") } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/DetektPlugin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/DetektPlugin.kt new file mode 100644 index 00000000..42c92d3c --- /dev/null +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/DetektPlugin.kt @@ -0,0 +1,48 @@ +package org.publicvalue.convention + +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +@Suppress("unused") +internal class DetektPlugin : Plugin { + override fun apply(target: Project) { + val settingsPath = target.rootProject.file("settings.gradle.kts").absolutePath + val projectBuildGradlePath = target.rootProject.file("build.gradle.kts").absolutePath + val buildLogic = target.rootProject.file("build-logic/convention/src").absolutePath + target.subprojects { + val targetBuildGradlePath = file("build.gradle.kts").absolutePath + afterEvaluate { + plugins.apply(libs.plugins.detekt.get().pluginId) + + configure { + buildUponDefaultConfig = true + autoCorrect = true + baseline = file("code-quality/baseline.xml") + source.setFrom( + "src/main/kotlin", + "src/commonMain/kotlin", + "src/commonTest", + "src/iosMain", + "src/jvmMain", + "src/wasmJsMain", + "src/androidMain", + "src/nativeMain", + "src/jsMain", + targetBuildGradlePath, + projectBuildGradlePath, + settingsPath, + buildLogic + ) + } + + dependencies { + add("detektPlugins", libs.detekt.formatting) + add("detektPlugins", libs.detekt.rules.libraries) + } + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/KotlinMultiplatformConventionPlugin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/KotlinMultiplatformConventionPlugin.kt index f4a21fce..7d325977 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/KotlinMultiplatformConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/KotlinMultiplatformConventionPlugin.kt @@ -9,7 +9,8 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import org.publicvalue.convention.config.configureKotlin -class KotlinMultiplatformConventionPlugin : Plugin { +@Suppress("unused") +internal class KotlinMultiplatformConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { // use mobile plugin and add jvm target @@ -26,8 +27,13 @@ class KotlinMultiplatformConventionPlugin : Plugin { } } -fun Project.addKspDependencyForAllTargets(dependencyNotation: Any) = addKspDependencyForAllTargets("", dependencyNotation) -fun Project.addKspTestDependencyForAllTargets(dependencyNotation: Any) = addKspDependencyForAllTargets("Test", dependencyNotation) +fun Project.addKspDependencyForAllTargets(dependencyNotation: Any) = addKspDependencyForAllTargets( + "", + dependencyNotation +) +fun Project.addKspTestDependencyForAllTargets( + dependencyNotation: Any +) = addKspDependencyForAllTargets("Test", dependencyNotation) private fun Project.addKspDependencyForAllTargets( configurationNameSuffix: String, @@ -57,10 +63,10 @@ fun KotlinMultiplatformExtension.addParcelizeAnnotation(annotationClass: String) compilerOptions { freeCompilerArgs.addAll( "-P", - "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=${annotationClass}" + "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=$annotationClass" ) } } } -fun String.capitalized() = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file +fun String.capitalized() = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/KotlinMultiplatformMobileConventionPlugin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/KotlinMultiplatformMobileConventionPlugin.kt index e44eb322..e7823fa9 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/KotlinMultiplatformMobileConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/KotlinMultiplatformMobileConventionPlugin.kt @@ -5,12 +5,12 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.publicvalue.convention.config.configureAndroidTarget -import org.publicvalue.convention.config.configureKotlin /** * No JVM target, only android + ios */ -class KotlinMultiplatformMobileConventionPlugin : Plugin { +@Suppress("unused") +internal class KotlinMultiplatformMobileConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { @@ -19,6 +19,7 @@ class KotlinMultiplatformMobileConventionPlugin : Plugin { extensions.configure { applyDefaultHierarchyTemplate() + explicitApi() if (pluginManager.hasPlugin("com.android.library")) { this.configureAndroidTarget() @@ -27,4 +28,4 @@ class KotlinMultiplatformMobileConventionPlugin : Plugin { // configureKotlin() } } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/MavenCentralPublishConventionPlugin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/MavenCentralPublishConventionPlugin.kt index 909ef7f6..dfd6906a 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/MavenCentralPublishConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/MavenCentralPublishConventionPlugin.kt @@ -14,7 +14,8 @@ import org.gradle.plugins.signing.SigningExtension import org.jetbrains.compose.internal.utils.getLocalProperty import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -class MavenCentralPublishConventionPlugin : Plugin { +@Suppress("unused", "LongMethod") +internal class MavenCentralPublishConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { @@ -24,6 +25,7 @@ class MavenCentralPublishConventionPlugin : Plugin { } extensions.configure { + explicitApi() if (pluginManager.hasPlugin("com.android.library")) { androidTarget { publishLibraryVariants("release") @@ -31,7 +33,6 @@ class MavenCentralPublishConventionPlugin : Plugin { } } - val javadocJar = tasks.register("javadocJar", Jar::class.java) { archiveClassifier.set("javadoc") from(tasks.getByName("dokkaHtml")) @@ -72,7 +73,9 @@ class MavenCentralPublishConventionPlugin : Plugin { } scm { connection.set("scm:git:github.com/kalinjul/kotlin-multiplatform-oidc.git") - developerConnection.set("scm:git:ssh://github.com/kalinjul/kotlin-multiplatform-oidc.git") + developerConnection.set( + "scm:git:ssh://github.com/kalinjul/kotlin-multiplatform-oidc.git" + ) url.set("https://github.com/kalinjul/kotlin-multiplatform-oidc/tree/main") } } @@ -90,7 +93,6 @@ class MavenCentralPublishConventionPlugin : Plugin { sign(publishing.publications) } - //region Fix Gradle warning about signing tasks using publishing task outputs without explicit dependencies // https://github.com/gradle/gradle/issues/26091 tasks.withType().configureEach { @@ -100,4 +102,4 @@ class MavenCentralPublishConventionPlugin : Plugin { //endregion } } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/MultiplatformSwiftPackageConventionPlugin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/MultiplatformSwiftPackageConventionPlugin.kt index 52672aef..e5e5ee96 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/MultiplatformSwiftPackageConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/MultiplatformSwiftPackageConventionPlugin.kt @@ -9,7 +9,8 @@ import java.io.File /** * No JVM target, only android + ios */ -class MultiplatformSwiftPackageConventionPlugin : Plugin { +@Suppress("unused") +internal class MultiplatformSwiftPackageConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { @@ -20,7 +21,7 @@ class MultiplatformSwiftPackageConventionPlugin : Plugin { swiftToolsVersion("5.6") targetPlatforms { iOS { v("15") } - macOS {v("15") } + macOS { v("15") } tvOS { v("15") } } outputDirectory(File(project.projectDir, "build/swiftpackage")) @@ -31,11 +32,13 @@ class MultiplatformSwiftPackageConventionPlugin : Plugin { if (project.version == "develop") { remote("https://github.com/kalinjul/OpenIdConnectClient/raw/${project.version}") } else { - remote("https://github.com/kalinjul/OpenIdConnectClient/releases/download/${project.version}") + remote( + "https://github.com/kalinjul/OpenIdConnectClient/releases/download/${project.version}" + ) } } } } } } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/VersionCatalogue.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/VersionCatalogue.kt index 67cf76b5..9eae386a 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/VersionCatalogue.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/VersionCatalogue.kt @@ -4,5 +4,5 @@ import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project import org.gradle.kotlin.dsl.the -val Project.libs: LibrariesForLibs - get() = the() \ No newline at end of file +internal val Project.libs: LibrariesForLibs + get() = the() diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Android.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Android.kt index 81beb1a5..a8df82b4 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Android.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Android.kt @@ -9,7 +9,7 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.the import java.io.File -fun Project.configureKotlinAndroid(extension: CommonExtension<*, *, *, *, *,*>) { +fun Project.configureKotlinAndroid(extension: CommonExtension<*, *, *, *, *, *>) { val libs = the() if (extension is LibraryExtension) { @@ -39,4 +39,4 @@ fun Project.configureKotlinAndroid(extension: CommonExtension<*, *, *, *, *,*>) targetCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get()) } } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Java.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Java.kt index 207a0056..a36540f4 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Java.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Java.kt @@ -1,11 +1,10 @@ package org.publicvalue.convention.config -import org.publicvalue.convention.libs -import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginExtension import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.kotlin.dsl.configure +import org.publicvalue.convention.libs fun Project.configureJava() { java { diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Kotlin.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Kotlin.kt index dc255566..497a5be1 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Kotlin.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Kotlin.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension fun Project.configureKotlin() { configureJava() extensions.configure { + explicitApi() compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") } diff --git a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Multiplatform.kt b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Multiplatform.kt index 2cfa3fa3..b2b12dbf 100644 --- a/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Multiplatform.kt +++ b/build-logic/convention/src/main/kotlin/org/publicvalue/convention/config/Multiplatform.kt @@ -22,7 +22,7 @@ fun KotlinMultiplatformExtension.configureAndroidTarget() { @OptIn(ExperimentalWasmDsl::class) fun KotlinMultiplatformExtension.configureWasmTarget(baseName: String? = null) { wasmJs { - outputModuleName.set(baseName ?: project.path.substring(1).replace(":","-").replace("-","_")) + outputModuleName.set(baseName ?: project.path.substring(1).replace(":", "-").replace("-", "_")) browser { commonWebpackConfig { outputFileName = "$baseName.js" @@ -35,6 +35,11 @@ fun KotlinMultiplatformExtension.configureWasmTarget(baseName: String? = null) { } } } + testTask { + useKarma { + useChromeHeadless() + } + } } } } @@ -59,4 +64,4 @@ fun KotlinMultiplatformExtension.exportKdoc() { compilerOptions.freeCompilerArgs.add("-Xexport-kdoc") } } -} \ No newline at end of file +} diff --git a/build-logic/scripts/pre-push b/build-logic/scripts/pre-push new file mode 100755 index 00000000..76dd03c9 --- /dev/null +++ b/build-logic/scripts/pre-push @@ -0,0 +1,11 @@ +#!/bin/sh +echo "Running detekt before commit..." +./gradlew detekt +RESULT=$? +if [ $RESULT -ne 0 ]; then + echo "❌ Code quality checks failed. Commit aborted." + echo "$RESULT" + exit 1 +fi +echo "✅ Code quality checks passed." +exit 0 diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 887927c4..959ae92c 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1,5 +1,7 @@ dependencyResolutionManagement { + @Suppress("UnstableApiUsage") repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + @Suppress("UnstableApiUsage") repositories { mavenCentral() google() diff --git a/build.gradle.kts b/build.gradle.kts index 48626d2d..771bf460 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.multiplatform.swiftpackage) apply false alias(libs.plugins.dokka) alias(libs.plugins.nexusPublish) + alias(libs.plugins.custom.detekt) } subprojects { @@ -21,7 +22,9 @@ nexusPublishing { sonatype { nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) - stagingProfileId.set(getLocalProperty("SONATYPE_STAGING_PROFILE_ID") ?: System.getenv("SONATYPE_STAGING_PROFILE_ID")) + stagingProfileId.set( + getLocalProperty("SONATYPE_STAGING_PROFILE_ID") ?: System.getenv("SONATYPE_STAGING_PROFILE_ID") + ) username.set(getLocalProperty("OSSRH_USERNAME") ?: System.getenv("OSSRH_USERNAME")) password.set(getLocalProperty("OSSRH_PASSWORD") ?: System.getenv("OSSRH_PASSWORD")) } diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 00000000..031b64ee --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,1080 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: false + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [] + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + - 'wasmJs' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 3 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' + +formatting: + active: true + android: false + autoCorrect: true + AnnotationOnSeparateLine: + active: true + autoCorrect: true + indentSize: 4 + AnnotationSpacing: + active: true + autoCorrect: true + ArgumentListWrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + BlockCommentInitialStarAlignment: + active: true + autoCorrect: true + ChainWrapping: + active: true + autoCorrect: true + indentSize: 4 + ClassName: + active: false + CommentSpacing: + active: true + autoCorrect: true + CommentWrapping: + active: true + autoCorrect: true + indentSize: 4 + ContextReceiverMapping: + active: false + autoCorrect: true + maxLineLength: 120 + indentSize: 4 + DiscouragedCommentLocation: + active: false + autoCorrect: true + EnumEntryNameCase: + active: true + autoCorrect: true + EnumWrapping: + active: false + autoCorrect: true + indentSize: 4 + Filename: + active: true + FinalNewline: + active: true + autoCorrect: true + insertFinalNewLine: true + FunKeywordSpacing: + active: true + autoCorrect: true + FunctionName: + active: false + FunctionReturnTypeSpacing: + active: true + autoCorrect: true + maxLineLength: 120 + FunctionSignature: + active: false + autoCorrect: true + forceMultilineWhenParameterCountGreaterOrEqualThan: 2147483647 + functionBodyExpressionWrapping: 'default' + maxLineLength: 120 + indentSize: 4 + FunctionStartOfBodySpacing: + active: true + autoCorrect: true + FunctionTypeReferenceSpacing: + active: true + autoCorrect: true + IfElseBracing: + active: false + autoCorrect: true + indentSize: 4 + IfElseWrapping: + active: false + autoCorrect: true + indentSize: 4 + ImportOrdering: + active: true + autoCorrect: true + layout: '*,java.**,javax.**,kotlin.**,^' + Indentation: + active: true + autoCorrect: true + indentSize: 4 + KdocWrapping: + active: true + autoCorrect: true + indentSize: 4 + MaximumLineLength: + active: true + maxLineLength: 120 + ignoreBackTickedIdentifier: false + ModifierListSpacing: + active: true + autoCorrect: true + ModifierOrdering: + active: true + autoCorrect: true + MultiLineIfElse: + active: true + autoCorrect: true + indentSize: 4 + MultilineExpressionWrapping: + active: false + autoCorrect: true + indentSize: 4 + NoBlankLineBeforeRbrace: + active: true + autoCorrect: true + NoBlankLineInList: + active: false + autoCorrect: true + NoBlankLinesInChainedMethodCalls: + active: true + autoCorrect: true + NoConsecutiveBlankLines: + active: true + autoCorrect: true + NoConsecutiveComments: + active: false + NoEmptyClassBody: + active: true + autoCorrect: true + NoEmptyFirstLineInClassBody: + active: false + autoCorrect: true + indentSize: 4 + NoEmptyFirstLineInMethodBlock: + active: true + autoCorrect: true + NoLineBreakAfterElse: + active: true + autoCorrect: true + NoLineBreakBeforeAssignment: + active: true + autoCorrect: true + NoMultipleSpaces: + active: true + autoCorrect: true + NoSemicolons: + active: true + autoCorrect: true + NoSingleLineBlockComment: + active: false + autoCorrect: true + indentSize: 4 + NoTrailingSpaces: + active: true + autoCorrect: true + NoUnitReturn: + active: true + autoCorrect: true + NoUnusedImports: + active: true + autoCorrect: true + NoWildcardImports: + active: true + packagesToUseImportOnDemandProperty: 'java.util.*,kotlinx.android.synthetic.**' + NullableTypeSpacing: + active: true + autoCorrect: true + PackageName: + active: true + autoCorrect: true + ParameterListSpacing: + active: false + autoCorrect: true + ParameterListWrapping: + active: true + autoCorrect: true + maxLineLength: 120 + indentSize: 4 + ParameterWrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + PropertyName: + active: false + PropertyWrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + SpacingAroundAngleBrackets: + active: true + autoCorrect: true + SpacingAroundColon: + active: true + autoCorrect: true + SpacingAroundComma: + active: true + autoCorrect: true + SpacingAroundCurly: + active: true + autoCorrect: true + SpacingAroundDot: + active: true + autoCorrect: true + SpacingAroundDoubleColon: + active: true + autoCorrect: true + SpacingAroundKeyword: + active: true + autoCorrect: true + SpacingAroundOperators: + active: true + autoCorrect: true + SpacingAroundParens: + active: true + autoCorrect: true + SpacingAroundRangeOperator: + active: true + autoCorrect: true + SpacingAroundUnaryOperator: + active: true + autoCorrect: true + SpacingBetweenDeclarationsWithAnnotations: + active: true + autoCorrect: true + SpacingBetweenDeclarationsWithComments: + active: true + autoCorrect: true + SpacingBetweenFunctionNameAndOpeningParenthesis: + active: true + autoCorrect: true + StringTemplate: + active: true + autoCorrect: true + StringTemplateIndent: + active: false + autoCorrect: true + indentSize: 4 + TrailingCommaOnCallSite: + active: false + autoCorrect: true + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: false + autoCorrect: true + useTrailingCommaOnDeclarationSite: true + TryCatchFinallySpacing: + active: false + autoCorrect: true + indentSize: 4 + TypeArgumentListSpacing: + active: false + autoCorrect: true + indentSize: 4 + TypeParameterListSpacing: + active: false + autoCorrect: true + indentSize: 4 + UnnecessaryParenthesesBeforeTrailingLambda: + active: true + autoCorrect: true + Wrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + +libraries: + active: true + ForbiddenPublicDataClass: + active: true + ignorePackages: + - '*.internal' + - '*.internal.*' + LibraryCodeMustSpecifyReturnType: + active: true + allowOmitUnit: false + LibraryEntitiesShouldNotBePublic: + active: false diff --git a/docs/ios/README.md b/docs/ios/README.md index a3e033a3..c17101d6 100644 --- a/docs/ios/README.md +++ b/docs/ios/README.md @@ -79,8 +79,8 @@ do { Perform refresh or endSession: ```swift -try await client.refreshToken(refreshToken: tokens.refresh_token!) -try await client.endSession(idToken: tokens.id_token!) +try await client.refreshToken(refreshToken: tokens.refreshToken!) +try await client.endSession(idToken: tokens.idToken!) ``` ## Custom headers/url parameters @@ -112,7 +112,7 @@ try await flow.getAccessToken( # JWT Parsing We provide simple JWT parsing: ```swift -let jwt = tokens.id_token.map { try! JwtParser.shared.parse(from: $0) } +let jwt = tokens.idToken.map { try! JwtParser.shared.parse(from: $0) } print(jwt?.payload.aud) // print audience print(jwt?.payload.iss) // print issuer print(jwt?.payload.additionalClaims["email"]) // get claim diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3eb156a2..3d487f94 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,10 @@ russhwolf = "1.3.0" kotlincrypto-hash = "0.8.0" material-icons = "1.7.3" +#Tooling +# https://github.com/detekt/detekt/releases +detekt = "1.23.8" + [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppCompat" } @@ -121,6 +125,10 @@ russhwolf-multiplatformsettings = { module = "com.russhwolf:multiplatform-settin kotlincrypto-hash-bom = { module = "org.kotlincrypto.hash:bom", version.ref = "kotlincrypto-hash" } kotlincrypto-hash-sha2 = { module = "org.kotlincrypto.hash:sha2", version.ref = "kotlincrypto-hash" } +detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } +detekt-rules-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } @@ -132,4 +140,6 @@ sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus-publish-plugin" } multiplatform-swiftpackage = { id = "io.github.luca992.multiplatform-swiftpackage", version.ref = "multiplatform-swiftpackage" } -dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } \ No newline at end of file +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +custom-detekt = { id = "org.publicvalue.convention.detekt" } \ No newline at end of file diff --git a/oidc-appsupport/build.gradle.kts b/oidc-appsupport/build.gradle.kts index 3764f5a2..8668c557 100644 --- a/oidc-appsupport/build.gradle.kts +++ b/oidc-appsupport/build.gradle.kts @@ -1,12 +1,8 @@ -import com.android.build.gradle.internal.tasks.factory.dependsOn import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.publicvalue.convention.config.configureIosTargets import org.publicvalue.convention.config.configureWasmTarget import org.publicvalue.convention.config.exportKdoc -import java.nio.file.Files -import java.util.stream.Collectors.toList -import kotlin.io.path.name plugins { id("org.publicvalue.convention.android.library") @@ -28,42 +24,24 @@ kotlin { configureIosTargets(baseName = "OpenIdConnectClient") configureWasmTarget(baseName = "OpenIdConnectClient") sourceSets { - val commonMain by getting { - dependencies { - api(projects.oidcCore) - api(projects.oidcTokenstore) - } + commonMain.dependencies { + api(projects.oidcCore) + api(projects.oidcTokenstore) } - val iosMain by getting { - dependencies { - } + androidMain.dependencies { + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.browser) } - val androidMain by getting { - dependencies { - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.browser) - } + jvmMain.dependencies { + implementation(libs.ktor.server.core) + implementation(libs.ktor.server.cio) } - val jvmMain by getting { - dependencies { - implementation(libs.ktor.server.core) - implementation(libs.ktor.server.cio) - } - } - - val wasmJsMain by getting { - dependencies { - implementation(libs.kotlinx.browser) - } - } - - val commonTest by getting { - dependencies { - } + wasmJsMain.dependencies { + implementation(libs.kotlinx.browser) } } @@ -73,11 +51,12 @@ kotlin { export(projects.oidcCore) export(projects.oidcTokenstore) -// freeCompilerArgs += listOf("-Xoverride-konan-properties=minVersion.ios=15.0;minVersionSinceXcode15.ios=15.0") + /*freeCompilerArgs += listOf("-Xoverride-konan-properties=minVersion.ios=15.0" + + ";minVersionSinceXcode15.ios=15.0")*/ } } } android { namespace = "org.publicvalue.multiplatform.oidc.appsupport" -} \ No newline at end of file +} diff --git a/oidc-appsupport/code-quality/baseline.xml b/oidc-appsupport/code-quality/baseline.xml new file mode 100644 index 00000000..26e9c966 --- /dev/null +++ b/oidc-appsupport/code-quality/baseline.xml @@ -0,0 +1,15 @@ + + + + + ForbiddenPublicDataClass:PlatformCodeAuthFlow.jvm.kt$UrlOpenException : Exception + ReturnCount:PlatformCodeAuthFlow.kt$@OptIn(ExperimentalContracts::class) internal fun <T> getErrorResult(responseUri: Url?): Result<T>? + SwallowedException:PlatformCodeAuthFlow.jvm.kt$e: Exception + SwallowedException:PlatformCodeAuthFlow.jvm.kt$e: SocketException + UnusedParameter:WebPopupFlow.kt$event: MessageEvent + UnusedParameter:WebPopupFlow.kt$targetOrigin: String + UnusedParameter:WebPopupFlow.kt$url: String + UnusedPrivateProperty:PlatformCodeAuthFlow.ios.kt$PlatformCodeAuthFlow$ephemeralBrowserSession: Boolean = false + Wrapping:PlatformCodeAuthFlow.ios.kt$PresentationContext$NSObject(), ASWebAuthenticationPresentationContextProvidingProtocol + + diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/AndroidCodeAuthFlowFactory.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/AndroidCodeAuthFlowFactory.kt index 42b2fd67..ca1d0731 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/AndroidCodeAuthFlowFactory.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/AndroidCodeAuthFlowFactory.kt @@ -23,8 +23,8 @@ import org.publicvalue.multiplatform.oidc.flows.EndSessionFlow * [Activity.onCreate()] using a non-null activity parameter or you must call registerActivity() * inside your Activity's [Activity.onCreate()]. */ -@Suppress("unused") -class AndroidCodeAuthFlowFactory( + +public class AndroidCodeAuthFlowFactory( /** * If `true`, an embedded WebView is used for the authorization flow. * This is generally **not recommended** due to security and UX concerns. @@ -40,9 +40,12 @@ class AndroidCodeAuthFlowFactory( * the flow in both WebView and Custom Tabs (if supported). */ private val ephemeralSession: Boolean = false, - /** preferred custom tab providers, list of package names in order of priority. Check [Browser][org.publicvalue.multiplatform.oidc.appsupport.customtab.Browser] for example values. **/ + /** preferred custom tab providers, list of package names in order of priority. + * Check [Browser][org.publicvalue.multiplatform.oidc.appsupport.customtab.Browser] + * for example values. + **/ private val customTabProviderPriority: List = listOf() -): CodeAuthFlowFactory { +) : CodeAuthFlowFactory { private lateinit var activityResultLauncher: ActivityResultLauncherSuspend private lateinit var context: Context @@ -51,21 +54,27 @@ class AndroidCodeAuthFlowFactory( @Deprecated( message = "Use AndroidCodeAuthFlowFactory(useWebView: Boolean) instead and call registerActivity().", - replaceWith = ReplaceWith("AndroidCodeAuthFlowFactory(useWebView).also { it.registerActivity(activity) }")) - constructor(activity: ComponentActivity, useWebView: Boolean = false) : this(useWebView = useWebView) { + replaceWith = ReplaceWith("AndroidCodeAuthFlowFactory(useWebView).also { it.registerActivity(activity) }") + ) + public constructor( + activity: ComponentActivity, + useWebView: Boolean = false + ) : this(useWebView = useWebView) { registerActivity(activity) } /** * Registers a lifecycle observer to be able to start a browser when required for login. */ - fun registerActivity(activity: ComponentActivity) { + public fun registerActivity(activity: ComponentActivity) { activity.lifecycle.addObserver( - object: LifecycleEventObserver { + object : LifecycleEventObserver { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { when (event) { Lifecycle.Event.ON_CREATE -> { - activityResultLauncher = activity.registerForActivityResultSuspend(resultFlow, ActivityResultContracts.StartActivityForResult()) + activityResultLauncher = activity.registerForActivityResultSuspend( + resultFlow, ActivityResultContracts.StartActivityForResult() + ) } Lifecycle.Event.ON_DESTROY -> { @@ -73,7 +82,6 @@ class AndroidCodeAuthFlowFactory( } else -> { - } } } @@ -88,7 +96,9 @@ class AndroidCodeAuthFlowFactory( val presentPreferredProviders = customTabProviderPriority.filter { customTabProviders.contains(it) } presentPreferredProviders.firstOrNull() - } else customTabProviders.firstOrNull() + } else { + customTabProviders.firstOrNull() + } val webFlow = if (useWebView) { WebViewFlow( diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/HandleRedirectActivity.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/HandleRedirectActivity.kt index 5458dcc8..e82651ef 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/HandleRedirectActivity.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/HandleRedirectActivity.kt @@ -25,7 +25,7 @@ internal const val EXTRA_KEY_REDIRECTURL = "redirecturl" internal const val EXTRA_KEY_URL = "url" internal const val EXTRA_KEY_PACKAGE_NAME = "package" -class HandleRedirectActivity : ComponentActivity() { +internal class HandleRedirectActivity : ComponentActivity() { companion object { /** Set to use your own web settings when using WebView **/ @@ -45,7 +45,6 @@ class HandleRedirectActivity : ComponentActivity() { @ExperimentalOpenIdConnect var configureWebView: (WebView) -> Unit = defaultConfigureWebView - @ExperimentalOpenIdConnect var createWebView: ComponentActivity.(redirectUrl: String?) -> WebView = { redirectUrl -> WebView(this).apply { @@ -57,7 +56,9 @@ class HandleRedirectActivity : ComponentActivity() { request: WebResourceRequest? ): Boolean { val requestedUrl = request?.url - return if (requestedUrl != null && redirectUrl != null && requestedUrl.toString().startsWith(redirectUrl)) { + return if (requestedUrl != null && redirectUrl != null && + requestedUrl.toString().startsWith(redirectUrl) + ) { intent.data = request.url setResult(RESULT_OK, intent) finish() @@ -71,10 +72,16 @@ class HandleRedirectActivity : ComponentActivity() { } @ExperimentalOpenIdConnect - var showWebView: ComponentActivity.(url: String, redirectUrl: String?, epheremalSession: Boolean) -> Unit = { url, redirectUrl, epheremalSession -> + var showWebView: ComponentActivity.( + url: String, + redirectUrl: String?, + epheremalSession: Boolean + ) -> Unit = { url, redirectUrl, epheremalSession -> val webView = createWebView(this, redirectUrl) ViewCompat.setOnApplyWindowInsetsListener(webView) { view, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()) + val insets = windowInsets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + ) view.updateLayoutParams { topMargin = insets.top leftMargin = insets.left @@ -168,7 +175,6 @@ class HandleRedirectActivity : ComponentActivity() { return } - val intent = builder.build() preferredBrowserPackage.let { intent.intent.setPackage(it) } @@ -184,4 +190,4 @@ class HandleRedirectActivity : ComponentActivity() { super.onNewIntent(intent) setIntent(intent) } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.android.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.android.kt index df337be6..bebca3e9 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.android.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.android.kt @@ -10,14 +10,15 @@ import org.publicvalue.multiplatform.oidc.flows.EndSessionResponse import org.publicvalue.multiplatform.oidc.types.AuthCodeRequest import org.publicvalue.multiplatform.oidc.types.EndSessionRequest -actual class PlatformCodeAuthFlow internal constructor( +public actual class PlatformCodeAuthFlow internal constructor( private val webFlow: WebAuthenticationFlow, actual override val client: OpenIdConnectClient, ) : CodeAuthFlow, EndSessionFlow { // TODO extract common code actual override suspend fun getAuthorizationCode(request: AuthCodeRequest): AuthCodeResponse { - val result = webFlow.startWebFlow(request.url, request.url.parameters.get("redirect_uri").orEmpty()) + val result = + webFlow.startWebFlow(request.url, request.url.parameters.get("redirect_uri").orEmpty()) return if (result is WebAuthenticationFlowResult.Success) { when (val error = getErrorResult(result.responseUri)) { @@ -26,6 +27,7 @@ actual class PlatformCodeAuthFlow internal constructor( val code = result.responseUri.parameters.get("code") Result.success(AuthCodeResult(code, state)) } + else -> { return error } @@ -37,13 +39,17 @@ actual class PlatformCodeAuthFlow internal constructor( } actual override suspend fun endSession(request: EndSessionRequest): EndSessionResponse { - val result = webFlow.startWebFlow(request.url, request.url.parameters.get("post_logout_redirect_uri").orEmpty()) + val result = webFlow.startWebFlow( + request.url, + request.url.parameters.get("post_logout_redirect_uri").orEmpty() + ) return if (result is WebAuthenticationFlowResult.Success) { when (val error = getErrorResult(result.responseUri)) { null -> { return EndSessionResponse.success(Unit) } + else -> { return error } diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/StartActivityForResultSuspend.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/StartActivityForResultSuspend.kt index 24d50bbd..a5b777af 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/StartActivityForResultSuspend.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/StartActivityForResultSuspend.kt @@ -15,11 +15,10 @@ import kotlinx.coroutines.flow.first * @param resultFlow a MutableStateFlow(null) that should live longer than the activity * @param contract the contract */ -fun ComponentActivity.registerForActivityResultSuspend( +internal fun ComponentActivity.registerForActivityResultSuspend( resultFlow: MutableStateFlow = MutableStateFlow(null), contract: ActivityResultContract ): ActivityResultLauncherSuspend { - val delegate = registerForActivityResult(contract) { resultFlow.value = it } @@ -30,10 +29,10 @@ fun ComponentActivity.registerForActivityResultSuspend( ) } -class ActivityResultLauncherSuspend( +internal class ActivityResultLauncherSuspend( val delegate: ActivityResultLauncher, val resultFlow: MutableStateFlow, -): ActivityResultLauncher() { +) : ActivityResultLauncher() { override fun launch(input: Input, options: ActivityOptionsCompat?) { delegate.launch(input, options) @@ -51,4 +50,4 @@ class ActivityResultLauncherSuspend( } override val contract: ActivityResultContract = delegate.contract -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/Browser.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/Browser.kt index 3e6c745a..d969b498 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/Browser.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/Browser.kt @@ -1,19 +1,17 @@ package org.publicvalue.multiplatform.oidc.appsupport.customtab -@Suppress("unused") -object Browser { - const val CHROME = "com.android.chrome" - const val CHROME_BETA = "com.chrome.beta" - const val FIREFOX = "org.mozilla.firefox" - const val FIREFOX_BETA = "org.mozilla.firefox_beta" - const val FIREFOX_KLAR = "org.mozilla.klar" - const val SAMSUNG = "com.sec.android.app.sbrowser" - const val BRAVE = "com.brave.browser" - const val DUCKDUCKGO = "com.duckduckgo.mobile.android" - const val ECOSIA = "com.ecosia.android" - const val EDGE = "com.microsoft.emmx" - const val VIVALDI = "com.vivaldi.browser" - const val YANDEX = "com.yandex.browser" - const val TOR = "org.torproject.torbrowser" +public object Browser { + public const val CHROME: String = "com.android.chrome" + public const val CHROME_BETA: String = "com.chrome.beta" + public const val FIREFOX: String = "org.mozilla.firefox" + public const val FIREFOX_BETA: String = "org.mozilla.firefox_beta" + public const val FIREFOX_KLAR: String = "org.mozilla.klar" + public const val SAMSUNG: String = "com.sec.android.app.sbrowser" + public const val BRAVE: String = "com.brave.browser" + public const val DUCKDUCKGO: String = "com.duckduckgo.mobile.android" + public const val ECOSIA: String = "com.ecosia.android" + public const val EDGE: String = "com.microsoft.emmx" + public const val VIVALDI: String = "com.vivaldi.browser" + public const val YANDEX: String = "com.yandex.browser" + public const val TOR: String = "org.torproject.torbrowser" } - diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/Context+getCustomTabProviders.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/ContextExtensions.kt similarity index 90% rename from oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/Context+getCustomTabProviders.kt rename to oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/ContextExtensions.kt index 83d2d037..3467310e 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/Context+getCustomTabProviders.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/ContextExtensions.kt @@ -7,7 +7,7 @@ import android.content.pm.ResolveInfo import android.os.Build import androidx.core.net.toUri -private val ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService" +private const val ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService" internal fun Context.getCustomTabProviders(): List { val activityIntent = Intent(Intent.ACTION_VIEW, "http://www.example.com".toUri()) @@ -25,4 +25,4 @@ internal fun Context.getCustomTabProviders(): List { packageManager.resolveService(serviceIntent, 0) != null } return customTabProviders -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/CustomTabFlow.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/CustomTabFlow.kt index 806283bc..5b700c0d 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/CustomTabFlow.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/customtab/CustomTabFlow.kt @@ -19,7 +19,7 @@ internal class CustomTabFlow( private val contract: ActivityResultLauncherSuspend, private val epheremalSession: Boolean, private val preferredBrowserPackage: String?, -): WebAuthenticationFlow { +) : WebAuthenticationFlow { override suspend fun startWebFlow(requestUrl: Url, redirectUrl: String): WebAuthenticationFlowResult { val intent = prepareIntent(requestUrl = requestUrl.toString(), redirectUrl = redirectUrl) val result = contract.launchSuspend(intent) @@ -39,4 +39,4 @@ internal class CustomTabFlow( } return intent } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/util/ActivityResult+toAuthenticationFlowResult.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/util/ActivityResultExtensions.kt similarity index 99% rename from oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/util/ActivityResult+toAuthenticationFlowResult.kt rename to oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/util/ActivityResultExtensions.kt index 50179d29..25cc0fae 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/util/ActivityResult+toAuthenticationFlowResult.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/util/ActivityResultExtensions.kt @@ -10,4 +10,4 @@ internal fun ActivityResult.toAuthenticationFlowResult(): WebAuthenticationFlowR Activity.RESULT_OK -> WebAuthenticationFlowResult.Success(this.data?.data?.let { Url(it.toString()) }) else -> WebAuthenticationFlowResult.Cancelled } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webview/WebViewFlow.kt b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webview/WebViewFlow.kt index 505b6fb0..5312432c 100644 --- a/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webview/WebViewFlow.kt +++ b/oidc-appsupport/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webview/WebViewFlow.kt @@ -3,15 +3,22 @@ package org.publicvalue.multiplatform.oidc.appsupport.webview import android.content.Context import android.content.Intent import androidx.activity.result.ActivityResult -import io.ktor.http.* -import org.publicvalue.multiplatform.oidc.appsupport.* +import io.ktor.http.Url +import org.publicvalue.multiplatform.oidc.appsupport.ActivityResultLauncherSuspend +import org.publicvalue.multiplatform.oidc.appsupport.EXTRA_KEY_EPHEMERAL_SESSION +import org.publicvalue.multiplatform.oidc.appsupport.EXTRA_KEY_REDIRECTURL +import org.publicvalue.multiplatform.oidc.appsupport.EXTRA_KEY_URL +import org.publicvalue.multiplatform.oidc.appsupport.EXTRA_KEY_USEWEBVIEW +import org.publicvalue.multiplatform.oidc.appsupport.HandleRedirectActivity +import org.publicvalue.multiplatform.oidc.appsupport.WebAuthenticationFlow +import org.publicvalue.multiplatform.oidc.appsupport.WebAuthenticationFlowResult import org.publicvalue.multiplatform.oidc.appsupport.util.toAuthenticationFlowResult internal class WebViewFlow( private val context: Context, private val contract: ActivityResultLauncherSuspend, private val epheremalSession: Boolean, -): WebAuthenticationFlow { +) : WebAuthenticationFlow { override suspend fun startWebFlow(requestUrl: Url, redirectUrl: String): WebAuthenticationFlowResult { val intent = prepareIntent(requestUrl = requestUrl.toString(), redirectUrl = redirectUrl) val result = contract.launchSuspend(intent) @@ -31,4 +38,4 @@ internal class WebViewFlow( } return intent } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/androidUnitTest/kotlin/README.kt b/oidc-appsupport/src/androidUnitTest/kotlin/README.kt index c991f760..fd06ad7d 100644 --- a/oidc-appsupport/src/androidUnitTest/kotlin/README.kt +++ b/oidc-appsupport/src/androidUnitTest/kotlin/README.kt @@ -58,21 +58,21 @@ object README { } // Request access token using code auth flow - suspend fun `Request_access_token_using_code_auth_flow`() { + suspend fun `Request_accessToken_using_code_auth_flow`() { val flow = authFlowFactory.createAuthFlow(client) val tokens = flow.getAccessToken() } // perform refresh or endSession suspend fun `perform_refresh_or_endSession`() { - tokens.refresh_token?.let { client.refreshToken(refreshToken = it) } - tokens.id_token?.let { client.endSession(idToken = it) } + tokens.refreshToken?.let { client.refreshToken(refreshToken = it) } + tokens.idToken?.let { client.endSession(idToken = it) } } // endSession using web flow suspend fun `perform_endSession_getrequest`() { val flow = authFlowFactory.createEndSessionFlow(client) - tokens.id_token?.let { flow.endSession(it) } + tokens.idToken?.let { flow.endSession(it) } } // Custom headers/url parameters @@ -96,7 +96,7 @@ object README { // We provide simple JWT parsing fun `We_provide_simple_JWT_parsing`() { val tokens = AccessTokenResponse("abc") - val jwt = tokens.id_token?.parseJwt() + val jwt = tokens.idToken?.parseJwt() println(jwt?.payload?.aud) // print audience println(jwt?.payload?.iss) // print issuer println(jwt?.payload?.additionalClaims?.get("email")) // get claim diff --git a/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/CodeAuthFlowFactory.kt b/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/CodeAuthFlowFactory.kt index 2d9db892..b3089e6e 100644 --- a/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/CodeAuthFlowFactory.kt +++ b/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/CodeAuthFlowFactory.kt @@ -8,7 +8,7 @@ import kotlin.native.HiddenFromObjC @OptIn(ExperimentalObjCRefinement::class) @HiddenFromObjC -interface CodeAuthFlowFactory { - fun createAuthFlow(client: OpenIdConnectClient): CodeAuthFlow - fun createEndSessionFlow(client: OpenIdConnectClient): EndSessionFlow -} \ No newline at end of file +public interface CodeAuthFlowFactory { + public fun createAuthFlow(client: OpenIdConnectClient): CodeAuthFlow + public fun createEndSessionFlow(client: OpenIdConnectClient): EndSessionFlow +} diff --git a/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.kt b/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.kt index 05bb1dc0..f46dc162 100644 --- a/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.kt +++ b/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.kt @@ -12,7 +12,7 @@ import org.publicvalue.multiplatform.oidc.types.EndSessionRequest import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -expect class PlatformCodeAuthFlow: CodeAuthFlow, EndSessionFlow { +internal expect class PlatformCodeAuthFlow : CodeAuthFlow, EndSessionFlow { // in kotlin 2.0, we need to implement methods in expect classes override suspend fun getAuthorizationCode(request: AuthCodeRequest): AuthCodeResponse override suspend fun endSession(request: EndSessionRequest): EndSessionResponse @@ -34,7 +34,11 @@ internal fun getErrorResult(responseUri: Url?): Result? { ) } } else { - return Result.failure(OpenIdConnectException.AuthenticationFailure(message = "No Uri in callback from browser (was ${responseUri}).")) + return Result.failure( + OpenIdConnectException.AuthenticationFailure( + message = "No Uri in callback from browser (was $responseUri)." + ) + ) } return null -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebAuthenticationFlow.kt b/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebAuthenticationFlow.kt index 1c42c7a1..9e899d10 100644 --- a/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebAuthenticationFlow.kt +++ b/oidc-appsupport/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebAuthenticationFlow.kt @@ -3,10 +3,10 @@ package org.publicvalue.multiplatform.oidc.appsupport import io.ktor.http.Url internal sealed class WebAuthenticationFlowResult { - data class Success(val responseUri: Url?): WebAuthenticationFlowResult() - data object Cancelled: WebAuthenticationFlowResult() + data class Success(val responseUri: Url?) : WebAuthenticationFlowResult() + data object Cancelled : WebAuthenticationFlowResult() } internal interface WebAuthenticationFlow { suspend fun startWebFlow(requestUrl: Url, redirectUrl: String): WebAuthenticationFlowResult -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/IosCodeAuthFlowFactory.kt b/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/IosCodeAuthFlowFactory.kt index 957fd88b..307560dd 100644 --- a/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/IosCodeAuthFlowFactory.kt +++ b/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/IosCodeAuthFlowFactory.kt @@ -6,10 +6,9 @@ import kotlin.experimental.ExperimentalObjCRefinement @OptIn(ExperimentalObjCRefinement::class) @HiddenFromObjC -@Suppress("unused") -class IosCodeAuthFlowFactory( +public class IosCodeAuthFlowFactory( private val ephemeralBrowserSession: Boolean = false -): CodeAuthFlowFactory { +) : CodeAuthFlowFactory { override fun createAuthFlow(client: OpenIdConnectClient): PlatformCodeAuthFlow { return PlatformCodeAuthFlow( client = client, @@ -22,4 +21,4 @@ class IosCodeAuthFlowFactory( override fun createEndSessionFlow(client: OpenIdConnectClient): EndSessionFlow { return createAuthFlow(client) } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.ios.kt b/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.ios.kt index 28c5412e..e5f6dfb9 100644 --- a/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.ios.kt +++ b/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.ios.kt @@ -28,66 +28,84 @@ import kotlin.experimental.ExperimentalObjCName */ @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "CodeAuthFlow", name = "CodeAuthFlow", exact = true) -actual class PlatformCodeAuthFlow internal constructor( +public actual class PlatformCodeAuthFlow internal constructor( actual override val client: OpenIdConnectClient, ephemeralBrowserSession: Boolean = false, private val webFlow: WebAuthenticationFlow, -): CodeAuthFlow, EndSessionFlow { +) : CodeAuthFlow, EndSessionFlow { - actual override suspend fun getAuthorizationCode(request: AuthCodeRequest): AuthCodeResponse = wrapExceptions { - val result = webFlow.startWebFlow(request.url, request.url.parameters.get("redirect_uri").orEmpty()) - return if (result is WebAuthenticationFlowResult.Success) { - when (val error = getErrorResult(result.responseUri)) { - null -> { - val state = result.responseUri?.parameters?.get("state") - val code = result.responseUri?.parameters?.get("code") - Result.success(AuthCodeResult(code, state)) - } - else -> { - return error + actual override suspend fun getAuthorizationCode(request: AuthCodeRequest): AuthCodeResponse = + wrapExceptions { + val result = webFlow.startWebFlow( + request.url, + request.url.parameters["redirect_uri"].orEmpty() + ) + return if (result is WebAuthenticationFlowResult.Success) { + when (val error = getErrorResult(result.responseUri)) { + null -> { + val state = result.responseUri?.parameters["state"] + val code = result.responseUri?.parameters["code"] + Result.success(AuthCodeResult(code, state)) + } + + else -> { + return error + } } + } else { + // browser closed, no redirect + Result.failure(OpenIdConnectException.AuthenticationCancelled()) } - } else { - // browser closed, no redirect - Result.failure(OpenIdConnectException.AuthenticationCancelled()) } - } - actual override suspend fun endSession(request: EndSessionRequest): EndSessionResponse = wrapExceptions { - val result = webFlow.startWebFlow(request.url, request.url.parameters.get("post_logout_redirect_uri").orEmpty()) + actual override suspend fun endSession(request: EndSessionRequest): EndSessionResponse = + wrapExceptions { + val result = webFlow.startWebFlow( + request.url, + request.url.parameters.get("post_logout_redirect_uri").orEmpty() + ) - return if (result is WebAuthenticationFlowResult.Success) { - when (val error = getErrorResult(result.responseUri)) { - null -> { - return EndSessionResponse.success(Unit) - } - else -> { - return error + return if (result is WebAuthenticationFlowResult.Success) { + return when (val error = getErrorResult(result.responseUri)) { + null -> { + EndSessionResponse.success(Unit) + } + + else -> { + error + } } + } else { + // browser closed, no redirect + EndSessionResponse.failure(OpenIdConnectException.AuthenticationCancelled("Logout cancelled")) } - } else { - // browser closed, no redirect - EndSessionResponse.failure(OpenIdConnectException.AuthenticationCancelled("Logout cancelled")) } - } } -class PresentationContext: NSObject(), ASWebAuthenticationPresentationContextProvidingProtocol { - override fun presentationAnchorForWebAuthenticationSession(session: ASWebAuthenticationSession): ASPresentationAnchor { +internal class PresentationContext : + NSObject(), + ASWebAuthenticationPresentationContextProvidingProtocol { + override fun presentationAnchorForWebAuthenticationSession( + session: ASWebAuthenticationSession + ): ASPresentationAnchor { return ASPresentationAnchor() } } -/** fix for multiple callbacks from ASWebAuthenticationSession (https://github.com/kalinjul/kotlin-multiplatform-oidc/issues/89) **/ -fun CancellableContinuation.resumeIfActive(value: T) { +/** fix for multiple callbacks from + * ASWebAuthenticationSession (https://github.com/kalinjul/kotlin-multiplatform-oidc/issues/89) + * **/ +internal fun CancellableContinuation.resumeIfActive(value: T) { if (isActive) { resume(value) } } -/** fix for multiple callbacks from ASWebAuthenticationSession (https://github.com/kalinjul/kotlin-multiplatform-oidc/issues/89) **/ -fun CancellableContinuation.resumeWithExceptionIfActive(value: Exception) { +/** fix for multiple callbacks + * from ASWebAuthenticationSession (https://github.com/kalinjul/kotlin-multiplatform-oidc/issues/89) + **/ +internal fun CancellableContinuation.resumeWithExceptionIfActive(value: Exception) { if (isActive) { resumeWithException(value) } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebSessionFlow.kt b/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebSessionFlow.kt index 1616b0ae..10206da8 100644 --- a/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebSessionFlow.kt +++ b/oidc-appsupport/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebSessionFlow.kt @@ -12,7 +12,7 @@ import platform.Foundation.NSURL internal class WebSessionFlow( private val ephemeralBrowserSession: Boolean, -): WebAuthenticationFlow { +) : WebAuthenticationFlow { /** * @return null if user cancelled the flow (closed the web view) */ @@ -46,4 +46,4 @@ internal class WebSessionFlow( } } } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/JvmCodeAuthFlowFactory.kt b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/JvmCodeAuthFlowFactory.kt index 45e7194a..a48f8b42 100644 --- a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/JvmCodeAuthFlowFactory.kt +++ b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/JvmCodeAuthFlowFactory.kt @@ -1,18 +1,17 @@ package org.publicvalue.multiplatform.oidc.appsupport -import io.ktor.http.* +import io.ktor.http.Url import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect import org.publicvalue.multiplatform.oidc.OpenIdConnectClient import org.publicvalue.multiplatform.oidc.appsupport.webserver.SimpleKtorWebserver import org.publicvalue.multiplatform.oidc.appsupport.webserver.Webserver import org.publicvalue.multiplatform.oidc.flows.EndSessionFlow -@Suppress("unused") @ExperimentalOpenIdConnect -class JvmCodeAuthFlowFactory( +public class JvmCodeAuthFlowFactory( private val webserverProvider: () -> Webserver = { SimpleKtorWebserver() }, private val openUrl: (Url) -> Unit = { it.openInBrowser() }, -): CodeAuthFlowFactory { +) : CodeAuthFlowFactory { override fun createAuthFlow(client: OpenIdConnectClient): PlatformCodeAuthFlow { return PlatformCodeAuthFlow( client = client, @@ -26,4 +25,4 @@ class JvmCodeAuthFlowFactory( override fun createEndSessionFlow(client: OpenIdConnectClient): EndSessionFlow { return createAuthFlow(client) } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.jvm.kt b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.jvm.kt index d54b9648..c50d348c 100644 --- a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.jvm.kt +++ b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.jvm.kt @@ -1,11 +1,10 @@ package org.publicvalue.multiplatform.oidc.appsupport -import io.ktor.http.* +import io.ktor.http.Url +import io.ktor.http.toURI import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect import org.publicvalue.multiplatform.oidc.OpenIdConnectClient import org.publicvalue.multiplatform.oidc.OpenIdConnectException -import org.publicvalue.multiplatform.oidc.appsupport.webserver.SimpleKtorWebserver -import org.publicvalue.multiplatform.oidc.appsupport.webserver.Webserver import org.publicvalue.multiplatform.oidc.flows.AuthCodeResponse import org.publicvalue.multiplatform.oidc.flows.AuthCodeResult import org.publicvalue.multiplatform.oidc.flows.CodeAuthFlow @@ -21,7 +20,7 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @ExperimentalOpenIdConnect -actual class PlatformCodeAuthFlow internal constructor( +public actual class PlatformCodeAuthFlow internal constructor( actual override val client: OpenIdConnectClient, private val webFlow: WebAuthenticationFlow ) : CodeAuthFlow, EndSessionFlow { @@ -63,12 +62,14 @@ actual class PlatformCodeAuthFlow internal constructor( returns() implies (redirectUrl != null) } if (redirectUrl?.isLocalhost() == false) { - throw OpenIdConnectException.AuthenticationFailure("JVM implementation can only handle redirect uris using localhost! Redirect uri was: $redirectUrl") + throw OpenIdConnectException.AuthenticationFailure( + "JVM implementation can only handle redirect uris using localhost! Redirect uri was: $redirectUrl" + ) } } } -fun Url.isLocalhost(): Boolean { +public fun Url.isLocalhost(): Boolean { return try { val address = InetAddress.getByName(host) if (address.isAnyLocalAddress || address.isLoopbackAddress) { @@ -84,13 +85,12 @@ fun Url.isLocalhost(): Boolean { } } -fun Url.openInBrowser() { +public fun Url.openInBrowser() { val desktop = if (Desktop.isDesktopSupported()) Desktop.getDesktop() else null if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { try { desktop.browse(toURI()) } catch (e: Exception) { - e.printStackTrace() throw UrlOpenException(e.message, cause = e) } } else { @@ -98,6 +98,7 @@ fun Url.openInBrowser() { } } -data class UrlOpenException( - override val message: String?, override val cause: Throwable? = null -): Exception(message, cause) \ No newline at end of file +public data class UrlOpenException( + override val message: String?, + override val cause: Throwable? = null +) : Exception(message, cause) diff --git a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebServerFlow.kt b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebServerFlow.kt index 99f19938..202a668a 100644 --- a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebServerFlow.kt +++ b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebServerFlow.kt @@ -1,4 +1,4 @@ -package org.publicvalue.multiplatform.oidc.appsupport; +package org.publicvalue.multiplatform.oidc.appsupport import io.ktor.http.Url import kotlinx.coroutines.Dispatchers @@ -11,7 +11,7 @@ import org.publicvalue.multiplatform.oidc.appsupport.webserver.Webserver internal class WebServerFlow( private val webserverProvider: () -> Webserver, private val openUrl: (Url) -> Unit, -): WebAuthenticationFlow { +) : WebAuthenticationFlow { override suspend fun startWebFlow(requestUrl: Url, redirectUrl: String): WebAuthenticationFlowResult { val webserver = webserverProvider() val response = withContext(Dispatchers.IO) { diff --git a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webserver/SimpleKtorWebserver.kt b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webserver/SimpleKtorWebserver.kt index af1fb48f..8ad3ca2f 100644 --- a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webserver/SimpleKtorWebserver.kt +++ b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webserver/SimpleKtorWebserver.kt @@ -15,9 +15,9 @@ import io.ktor.server.routing.routing import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect @ExperimentalOpenIdConnect -class SimpleKtorWebserver( - val port: Int = 8080, - val createResponse: suspend RoutingContext.() -> Unit = { +public class SimpleKtorWebserver( + public val port: Int = 8080, + public val createResponse: suspend RoutingContext.() -> Unit = { call.respondText( status = HttpStatusCode.OK, text = """ @@ -35,7 +35,7 @@ class SimpleKtorWebserver( contentType = ContentType.parse("text/html") ) } -): Webserver { +) : Webserver { private var server: CIOApplicationEngine? = null override suspend fun startAndWaitForRedirect(redirectPath: String): Url { diff --git a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webserver/Webserver.kt b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webserver/Webserver.kt index 29917f30..0229dcae 100644 --- a/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webserver/Webserver.kt +++ b/oidc-appsupport/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/webserver/Webserver.kt @@ -4,16 +4,16 @@ import io.ktor.http.Url import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect @ExperimentalOpenIdConnect -interface Webserver { +public interface Webserver { /** * Start a local Webserver on the given port, waiting for the redirectPath to be called. * * @return Url the redirect was called with, including query parameters. */ - suspend fun startAndWaitForRedirect(redirectPath: String): Url + public suspend fun startAndWaitForRedirect(redirectPath: String): Url /** * Stop the webserver. */ - suspend fun stop() + public suspend fun stop() } diff --git a/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.wasmJs.kt b/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.wasmJs.kt index d3c56c31..41620447 100644 --- a/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.wasmJs.kt +++ b/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/PlatformCodeAuthFlow.wasmJs.kt @@ -12,7 +12,7 @@ import org.publicvalue.multiplatform.oidc.types.AuthCodeRequest import org.publicvalue.multiplatform.oidc.types.EndSessionRequest @ExperimentalOpenIdConnect -actual class PlatformCodeAuthFlow( +public actual class PlatformCodeAuthFlow( windowTarget: String = "", windowFeatures: String = "width=1000,height=800,resizable=yes,scrollbars=yes", redirectOrigin: String, @@ -23,15 +23,19 @@ actual class PlatformCodeAuthFlow( @ExperimentalOpenIdConnect actual override suspend fun getAuthorizationCode(request: AuthCodeRequest): AuthCodeResponse { - val result = webFlow.startWebFlow(request.url, request.url.parameters.get("redirect_uri").orEmpty()) + val result = webFlow.startWebFlow( + request.url, + request.url.parameters["redirect_uri"].orEmpty() + ) return if (result is WebAuthenticationFlowResult.Success) { when (val error = getErrorResult(result.responseUri)) { null -> { - val state = result.responseUri.parameters.get("state") - val code = result.responseUri.parameters.get("code") + val state = result.responseUri.parameters["state"] + val code = result.responseUri.parameters["code"] Result.success(AuthCodeResult(code, state)) } + else -> { return error } @@ -43,16 +47,15 @@ actual class PlatformCodeAuthFlow( } actual override suspend fun endSession(request: EndSessionRequest): EndSessionResponse { - val redirectUrl = request.url.parameters.get("post_logout_redirect_uri").orEmpty() + val redirectUrl = request.url.parameters["post_logout_redirect_uri"].orEmpty() webFlow.startWebFlow(request.url, redirectUrl) return Result.success(Unit) } - companion object { + public companion object { @ExperimentalOpenIdConnect - fun handleRedirect() { + public fun handleRedirect() { WebPopupFlow.handleRedirect() } } } - diff --git a/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WasmCodeAuthFlowFactory.kt b/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WasmCodeAuthFlowFactory.kt index 2534f6a4..44cc9b90 100644 --- a/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WasmCodeAuthFlowFactory.kt +++ b/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WasmCodeAuthFlowFactory.kt @@ -6,11 +6,11 @@ import org.publicvalue.multiplatform.oidc.OpenIdConnectClient import org.publicvalue.multiplatform.oidc.flows.EndSessionFlow @ExperimentalOpenIdConnect -class WasmCodeAuthFlowFactory( +public class WasmCodeAuthFlowFactory( private val windowTarget: String = "", private val windowFeatures: String = "width=1000,height=800,resizable=yes,scrollbars=yes", private val redirectOrigin: String = window.location.origin -): CodeAuthFlowFactory { +) : CodeAuthFlowFactory { override fun createAuthFlow(client: OpenIdConnectClient): PlatformCodeAuthFlow { return PlatformCodeAuthFlow(windowTarget, windowFeatures, redirectOrigin, client) } @@ -18,4 +18,4 @@ class WasmCodeAuthFlowFactory( override fun createEndSessionFlow(client: OpenIdConnectClient): EndSessionFlow { return createAuthFlow(client) } -} \ No newline at end of file +} diff --git a/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebPopupFlow.kt b/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebPopupFlow.kt index 81a2ba6e..2073b202 100644 --- a/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebPopupFlow.kt +++ b/oidc-appsupport/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/appsupport/WebPopupFlow.kt @@ -1,6 +1,6 @@ package org.publicvalue.multiplatform.oidc.appsupport -import io.ktor.http.* +import io.ktor.http.Url import kotlinx.browser.window import kotlinx.serialization.json.Json import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect @@ -16,7 +16,7 @@ internal class WebPopupFlow( private val windowTarget: String = "", private val windowFeatures: String = "width=1000,height=800,resizable=yes,scrollbars=yes", private val redirectOrigin: String, -): WebAuthenticationFlow { +) : WebAuthenticationFlow { private class WindowHolder(var window: Window?) @@ -28,7 +28,6 @@ internal class WebPopupFlow( messageHandler = { event -> if (event is MessageEvent) { - if (event.origin != redirectOrigin) { throw TechnicalFailure("Security issue. Event was not from $redirectOrigin", null) } @@ -40,7 +39,10 @@ internal class WebPopupFlow( continuation.resume(WebAuthenticationFlowResult.Success(url)) } else { // Log an advisory but stay registered for the true callback - println("${WebPopupFlow::class.simpleName} skipping message from unknown source: ${event.source}") + println( + "${WebPopupFlow::class.simpleName} skipping message " + + "from unknown source: ${event.source}" + ) } } } @@ -53,6 +55,7 @@ internal class WebPopupFlow( } internal companion object { + @OptIn(ExperimentalWasmJsInterop::class) @ExperimentalOpenIdConnect fun handleRedirect() { if (window.opener != null) { @@ -75,6 +78,10 @@ private fun postMessage(url: String, targetOrigin: String) { js("window.opener.postMessage(url, targetOrigin)") } +@OptIn(ExperimentalWasmJsInterop::class) private fun closeWindow(delay: Int = 100) { - window.setTimeout(handler = { window.close(); null }, timeout = delay) -} \ No newline at end of file + window.setTimeout(handler = { + window.close() + null + }, timeout = delay) +} diff --git a/oidc-core/build.gradle.kts b/oidc-core/build.gradle.kts index 91de772e..b1481cb6 100644 --- a/oidc-core/build.gradle.kts +++ b/oidc-core/build.gradle.kts @@ -16,39 +16,31 @@ kotlin { configureIosTargets() configureWasmTarget() sourceSets { - val commonMain by getting { - dependencies { - implementation(libs.kotlinx.coroutines.core) + commonMain.dependencies { + implementation(libs.kotlinx.coroutines.core) - api(libs.ktor.client.core) - implementation(libs.kotlinx.serialization.json) - implementation(libs.ktor.client.contentnegotiation) - implementation(libs.ktor.serialization.kotlinx.json) + api(libs.ktor.client.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.ktor.client.contentnegotiation) + implementation(libs.ktor.serialization.kotlinx.json) - implementation(projects.oidcCrypto) - } + implementation(projects.oidcCrypto) } - val jvmMain by getting { - dependencies { - implementation(libs.ktor.client.okhttp) - } + jvmMain.dependencies { + implementation(libs.ktor.client.okhttp) } - val iosMain by getting { - dependencies { - implementation(libs.ktor.client.darwin) - } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - implementation(libs.assertk) - implementation(libs.kotlinx.coroutines.test) - } + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.assertk) + implementation(libs.kotlinx.coroutines.test) } } exportKdoc() -} \ No newline at end of file +} diff --git a/oidc-core/code-quality/baseline.xml b/oidc-core/code-quality/baseline.xml new file mode 100644 index 00000000..6488e7a1 --- /dev/null +++ b/oidc-core/code-quality/baseline.xml @@ -0,0 +1,28 @@ + + + + + Filename:JwtConvenienceExtensions.kt$org.publicvalue.multiplatform.oidc.types.JwtConvenienceExtensions.kt + ForbiddenPublicDataClass:AccessTokenResponse.kt$AccessTokenResponse + ForbiddenPublicDataClass:AuthCodeRequest.kt$AuthCodeRequest + ForbiddenPublicDataClass:AuthCodeResult.kt$AuthCodeResult + ForbiddenPublicDataClass:EndSessionRequest.kt$EndSessionRequest + ForbiddenPublicDataClass:ErrorResponse.kt$ErrorResponse + ForbiddenPublicDataClass:IdToken.kt$IdToken + ForbiddenPublicDataClass:Jwt.kt$Jwt + ForbiddenPublicDataClass:Jwt.kt$JwtHeader + ForbiddenPublicDataClass:OpenIdConnectClientConfig.kt$Endpoints + ForbiddenPublicDataClass:OpenIdConnectConfiguration.kt$OpenIdConnectConfiguration + ForbiddenPublicDataClass:TokenRequest.kt$TokenRequest + MatchingDeclarationName:JwtConvenienceExtensions.kt$JwtParser + MaxLineLength:JwtTest.kt$JwtTest$val + MaxLineLength:JwtTest.kt$JwtTest$val idToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg" + MaxLineLength:OpenIdConnectClientConfig.kt$OpenIdConnectClientConfig$* + ThrowsCount:CodeAuthFlow.kt$CodeAuthFlow$private suspend fun exchangeToken( client: OpenIdConnectClient, request: AuthCodeRequest, result: AuthCodeResult, configure: (HttpRequestBuilder.() -> Unit)? ): AccessTokenResponse + ThrowsCount:DefaultOpenIdConnectClient.kt$DefaultOpenIdConnectClient$private suspend fun executeTokenRequest(httpFormRequest: HttpStatement): AccessTokenResponse + ThrowsCount:OpenIdConnectClientConfig.kt$public fun OpenIdConnectClientConfig.validate() + TooGenericExceptionThrown:OpenIdConnectDiscover.kt$throw Exception("Could not download discovery document: $this") + VariableNaming:JwtTest.kt$JwtTest$@Suppress("MaximumLineLength") val idToken = "eyJhxbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg" + VariableNaming:JwtTest.kt$JwtTest$val idToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg" + + diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/DefaultOpenIdConnectClient.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/DefaultOpenIdConnectClient.kt index 307b0600..a61545ba 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/DefaultOpenIdConnectClient.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/DefaultOpenIdConnectClient.kt @@ -1,14 +1,25 @@ package org.publicvalue.multiplatform.oidc -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.client.statement.* -import io.ktor.http.* +import io.ktor.client.HttpClient +import io.ktor.client.call.HttpClientCall +import io.ktor.client.call.NoTransformationFoundException +import io.ktor.client.call.body +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.forms.prepareForm +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.url +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.HttpStatement +import io.ktor.http.ContentType +import io.ktor.http.ContentTypeMatcher +import io.ktor.http.HttpStatusCode +import io.ktor.http.URLBuilder +import io.ktor.http.decodeURLQueryComponent +import io.ktor.http.isSuccess +import io.ktor.http.parameters import io.ktor.serialization.JsonConvertException -import io.ktor.serialization.kotlinx.* +import io.ktor.serialization.kotlinx.KotlinxSerializationConverter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -29,16 +40,17 @@ import kotlin.native.ObjCName /** * OpenIdConnectClient implements the basic methods used to perform OpenID Connect Authentication. - * A client may also be constructed using the [Builder method][org.publicvalue.multiplatform.oidc.OpenIdConnectClient] + * A client may also be constructed using the [Builder method][OpenIdConnectClient] * * @param httpClient The (ktor) HTTP client to be used for code <-> token exchange and endSession requests. * Authentication is performed using [CodeAuthFlow][org.publicvalue.multiplatform.oidc.flows.CodeAuthFlow] * - * @param config [Configuration][org.publicvalue.multiplatform.oidc.OpenIdConnectClientConfig] for this client + * @param config [Configuration][OpenIdConnectClientConfig] for this client */ @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "OpenIdConnectClient", name = "OpenIdConnectClient", exact = true) -class DefaultOpenIdConnectClient( +@Suppress("TooManyFunctions") +public class DefaultOpenIdConnectClient( private val httpClient: HttpClient = DefaultHttpClient, override val config: OpenIdConnectClientConfig, ) : OpenIdConnectClient { @@ -46,14 +58,17 @@ class DefaultOpenIdConnectClient( * Swift convenience constructor * @suppress */ - constructor(config: OpenIdConnectClientConfig): this(httpClient = DefaultHttpClient, config = config) + public constructor(config: OpenIdConnectClientConfig) : this( + httpClient = DefaultHttpClient, + config = config + ) override var discoverDocument: OpenIdConnectConfiguration? = null private val scope by lazy { CoroutineScope(Dispatchers.Default + SupervisorJob()) } - companion object { - val DefaultHttpClient by lazy { + public companion object { + public val DefaultHttpClient: HttpClient by lazy { HttpClient { install(ContentNegotiation) { // register custom type matcher to support broken IDPs that don't send correct content-type @@ -87,27 +102,48 @@ class DefaultOpenIdConnectClient( val nonce = if (config.disableNonce) null else secureRandomBytes().encodeForPKCE() val state = secureRandomBytes().encodeForPKCE() - val authorizationEndpoint = config.endpoints?.authorizationEndpoint ?: run { throw OpenIdConnectException.InvalidConfiguration("No authorizationEndpoint set") } + val authorizationEndpoint = config.endpoints?.authorizationEndpoint ?: run { + throw OpenIdConnectException.InvalidConfiguration("No authorizationEndpoint set") + } val url = URLBuilder(authorizationEndpoint).apply { parameters.append("client_id", config.clientId!!) parameters.append("response_type", "code") parameters.append("response_mode", "query") config.scope?.let { parameters.append("scope", it) } nonce?.let { parameters.append("nonce", it) } - config.codeChallengeMethod.queryString?.let { parameters.append("code_challenge_method", it) } - if (config.codeChallengeMethod != CodeChallengeMethod.off) { parameters.append("code_challenge", pkce.codeChallenge) } + config.codeChallengeMethod.queryString?.let { + parameters.append( + "code_challenge_method", + it + ) + } + if (config.codeChallengeMethod != CodeChallengeMethod.OFF) { + parameters.append( + "code_challenge", + pkce.codeChallenge + ) + } config.redirectUri?.let { parameters.append("redirect_uri", it) } parameters.append("state", state) configure?.invoke(this) }.build() return AuthCodeRequest( - url, config, pkce, state, nonce + url, + config, + pkce, + state, + nonce ) } - override fun createEndSessionRequest(idToken: String?, configure: (URLBuilder.() -> Unit)?): EndSessionRequest { - val authorizationEndpoint = config.endpoints?.endSessionEndpoint ?: run { throw OpenIdConnectException.InvalidConfiguration("No endSessionEndpoint set") } + override fun createEndSessionRequest( + idToken: String?, + configure: (URLBuilder.() -> Unit)? + ): EndSessionRequest { + val authorizationEndpoint = config.endpoints?.endSessionEndpoint ?: run { + throw OpenIdConnectException.InvalidConfiguration("No endSessionEndpoint set") + } val url = URLBuilder(authorizationEndpoint).apply { idToken?.let { parameters.append("id_token_hint", it) } config.postLogoutRedirectUri?.let { parameters.append("post_logout_redirect_uri", it) } @@ -118,18 +154,23 @@ class DefaultOpenIdConnectClient( } @Throws(OpenIdConnectException::class, CancellationException::class) - override suspend fun discover(configure: (HttpRequestBuilder.() -> Unit)?) = wrapExceptions { - config.discoveryUri?.let { discoveryUri -> - val config = OpenIdConnectDiscover(httpClient).downloadConfiguration(discoveryUri, configure) - this.config.updateWithDiscovery(config) - discoverDocument = config - } ?: run { - throw OpenIdConnectException.InvalidUrl("No discoveryUri set") + override suspend fun discover(configure: (HttpRequestBuilder.() -> Unit)?): Unit = + wrapExceptions { + config.discoveryUri?.let { discoveryUri -> + val config = + OpenIdConnectDiscover(httpClient).downloadConfiguration(discoveryUri, configure) + this.config.updateWithDiscovery(config) + discoverDocument = config + } ?: run { + throw OpenIdConnectException.InvalidUrl("No discoveryUri set") + } } - } @Throws(OpenIdConnectException::class, CancellationException::class) - override suspend fun endSession(idToken: String, configure: (HttpRequestBuilder.() -> Unit)?): HttpStatusCode = wrapExceptions { + override suspend fun endSession( + idToken: String, + configure: (HttpRequestBuilder.() -> Unit)? + ): HttpStatusCode = wrapExceptions { val endpoint = config.endpoints?.endSessionEndpoint?.trim() if (!endpoint.isNullOrEmpty()) { val url = URLBuilder(endpoint) @@ -148,14 +189,24 @@ class DefaultOpenIdConnectClient( } @Throws(OpenIdConnectException::class, CancellationException::class) - override suspend fun revokeToken(token: String, configure: (HttpRequestBuilder.() -> Unit)?): HttpStatusCode = wrapExceptions { + override suspend fun revokeToken( + token: String, + configure: (HttpRequestBuilder.() -> Unit)? + ): HttpStatusCode = wrapExceptions { val endpoint = config.endpoints?.revocationEndpoint?.trim() if (!endpoint.isNullOrEmpty()) { val url = URLBuilder(endpoint) val response = httpClient.submitForm( formParameters = parameters { append("token", token) - append("client_id", config.clientId ?: run { throw OpenIdConnectException.InvalidConfiguration("clientId is missing") }) + append( + "client_id", + config.clientId ?: run { + throw OpenIdConnectException.InvalidConfiguration( + "clientId is missing" + ) + } + ) config.clientSecret?.let { append("client_secret", it) } } ) { @@ -169,29 +220,48 @@ class DefaultOpenIdConnectClient( } @Throws(OpenIdConnectException::class, CancellationException::class) - override suspend fun exchangeToken(authCodeRequest: AuthCodeRequest, code: String, configure: (HttpRequestBuilder.() -> Unit)?): AccessTokenResponse = wrapExceptions { + override suspend fun exchangeToken( + authCodeRequest: AuthCodeRequest, + code: String, + configure: (HttpRequestBuilder.() -> Unit)? + ): AccessTokenResponse = wrapExceptions { val tokenRequest = createAccessTokenRequest(authCodeRequest, code, configure) return executeTokenRequest(tokenRequest.request) } @Throws(OpenIdConnectException::class, CancellationException::class) - @Suppress("Unused") - override suspend fun refreshToken(refreshToken: String, configure: (HttpRequestBuilder.() -> Unit)?): AccessTokenResponse = wrapExceptions { + override suspend fun refreshToken( + refreshToken: String, + configure: (HttpRequestBuilder.() -> Unit)? + ): AccessTokenResponse = wrapExceptions { val tokenRequest = createRefreshTokenRequest(refreshToken, configure) return executeTokenRequest(tokenRequest.request) } @Throws(OpenIdConnectException::class, CancellationException::class) - override suspend fun createAccessTokenRequest(authCodeRequest: AuthCodeRequest, code: String, configure: (HttpRequestBuilder.() -> Unit)?): TokenRequest = wrapExceptions { + override suspend fun createAccessTokenRequest( + authCodeRequest: AuthCodeRequest, + code: String, + configure: (HttpRequestBuilder.() -> Unit)? + ): TokenRequest = wrapExceptions { val url = URLBuilder(getOrDiscoverTokenEndpoint()).build() val formParameters = parameters { append("grant_type", "authorization_code") append("code", code) config.redirectUri?.let { append("redirect_uri", it) } - append("client_id", config.clientId ?: run { throw OpenIdConnectException.InvalidConfiguration("clientId is missing") }) + append( + "client_id", + config.clientId + ?: run { throw OpenIdConnectException.InvalidConfiguration("clientId is missing") } + ) config.clientSecret?.let { append("client_secret", it) } - if (config.codeChallengeMethod != CodeChallengeMethod.off) { append("code_verifier", authCodeRequest.pkce.codeVerifier) } + if (config.codeChallengeMethod != CodeChallengeMethod.OFF) { + append( + "code_verifier", + authCodeRequest.pkce.codeVerifier + ) + } } val request = scope.async { // there is no suspending happening here httpClient.prepareForm( @@ -209,12 +279,19 @@ class DefaultOpenIdConnectClient( } @Throws(OpenIdConnectException::class, CancellationException::class) - override suspend fun createRefreshTokenRequest(refreshToken: String, configure: (HttpRequestBuilder.() -> Unit)?): TokenRequest = wrapExceptions { + override suspend fun createRefreshTokenRequest( + refreshToken: String, + configure: (HttpRequestBuilder.() -> Unit)? + ): TokenRequest = wrapExceptions { val url = URLBuilder(getOrDiscoverTokenEndpoint()).build() val formParameters = parameters { append("grant_type", "refresh_token") - append("client_id", config.clientId ?: run { throw OpenIdConnectException.InvalidConfiguration("clientId is missing") }) + append( + "client_id", + config.clientId + ?: run { throw OpenIdConnectException.InvalidConfiguration("clientId is missing") } + ) config.clientSecret?.let { append("client_secret", it) } append("refresh_token", refreshToken) config.scope?.let { append("scope", it) } @@ -258,11 +335,13 @@ class DefaultOpenIdConnectClient( } } - private suspend fun HttpResponse.toOpenIdConnectException(cause: Throwable? = null): OpenIdConnectException.UnsuccessfulTokenRequest { + private suspend fun HttpResponse.toOpenIdConnectException(cause: Throwable? = null): + OpenIdConnectException.UnsuccessfulTokenRequest { val errorResponse = call.errorBody() val body = call.body().decodeURLQueryComponent(plusIsSpace = true) return OpenIdConnectException.UnsuccessfulTokenRequest( - message = "Exchange token failed: ${status.value} ${errorResponse?.error_description ?: errorResponse?.error}", + message = "Exchange token failed: ${status.value} " + + "${errorResponse?.errorDescription ?: errorResponse?.error}", statusCode = status, body = body, errorResponse = errorResponse, @@ -274,7 +353,7 @@ class DefaultOpenIdConnectClient( private suspend fun HttpClientCall.errorBody(): ErrorResponse? { return try { body() - } catch (e: Exception) { + } catch (_: Exception) { null } -} \ No newline at end of file +} diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ExperimentalOpenIdConnect.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ExperimentalOpenIdConnect.kt index badbc594..f37e35c2 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ExperimentalOpenIdConnect.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ExperimentalOpenIdConnect.kt @@ -1,6 +1,9 @@ package org.publicvalue.multiplatform.oidc -@RequiresOptIn(message = "This API is experimental. It may be changed in the future without notice.", level = RequiresOptIn.Level.WARNING) +@RequiresOptIn( + message = "This API is experimental. It may be changed in the future without notice.", + level = RequiresOptIn.Level.WARNING +) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) -annotation class ExperimentalOpenIdConnect \ No newline at end of file +public annotation class ExperimentalOpenIdConnect diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClient.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClient.kt index a91508f7..99db4530 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClient.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClient.kt @@ -25,7 +25,7 @@ import kotlin.native.ObjCName * Setting an endpoint manually will override a discovered endpoint. * @param block configuration closure. See [OpenIdConnectClientConfig] */ -fun OpenIdConnectClient( +public fun OpenIdConnectClient( discoveryUri: String? = null, block: OpenIdConnectClientConfig.() -> Unit ): OpenIdConnectClient { @@ -40,25 +40,25 @@ fun OpenIdConnectClient( message = "Use DefaultOpenIdConnectClient constructor instead", replaceWith = ReplaceWith("DefaultOpenIdConnectClient(httpClient, config)") ) -fun OpenIdConnectClient( +public fun OpenIdConnectClient( httpClient: HttpClient = DefaultHttpClient, config: OpenIdConnectClientConfig, -) : OpenIdConnectClient { +): OpenIdConnectClient { return DefaultOpenIdConnectClient(httpClient = httpClient, config = config) } @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "OpenIdConnectClientProtocol", name = "OpenIdConnectClientProtocol", exact = true) -interface OpenIdConnectClient { - val config: OpenIdConnectClientConfig - val discoverDocument: OpenIdConnectConfiguration? +public interface OpenIdConnectClient { + public val config: OpenIdConnectClientConfig + public val discoverDocument: OpenIdConnectConfiguration? /** * Creates an Authorization Code Request which can then be executed by the * [CodeAuthFlow][org.publicvalue.multiplatform.oidc.flows.CodeAuthFlow]. */ @Throws(OpenIdConnectException::class) - fun createAuthorizationCodeRequest(configure: (URLBuilder.() -> Unit)? = null): AuthCodeRequest + public fun createAuthorizationCodeRequest(configure: (URLBuilder.() -> Unit)? = null): AuthCodeRequest /** * Creates an End Session Request which can then be executed by the @@ -67,7 +67,7 @@ interface OpenIdConnectClient { * @param idToken used for id_token_hint, recommended by openid spec, optional */ @Throws(OpenIdConnectException::class) - fun createEndSessionRequest(idToken: String?, configure: (URLBuilder.() -> Unit)? = null): EndSessionRequest + public fun createEndSessionRequest(idToken: String?, configure: (URLBuilder.() -> Unit)? = null): EndSessionRequest /** * Discover OpenID Connect Configuration using the discovery endpoint. @@ -78,7 +78,7 @@ interface OpenIdConnectClient { * @param configure configuration closure to configure the http request builder with */ @Throws(OpenIdConnectException::class, CancellationException::class) - suspend fun discover(configure: (HttpRequestBuilder.() -> Unit)? = null) + public suspend fun discover(configure: (HttpRequestBuilder.() -> Unit)? = null) /** * RP-initiated logout. @@ -90,7 +90,7 @@ interface OpenIdConnectClient { * be used for discovery if necessary) */ @Throws(OpenIdConnectException::class, CancellationException::class) - suspend fun endSession( + public suspend fun endSession( idToken: String, configure: (HttpRequestBuilder.() -> Unit)? = null ): HttpStatusCode @@ -104,7 +104,7 @@ interface OpenIdConnectClient { * be used for discovery if necessary) */ @Throws(OpenIdConnectException::class, CancellationException::class) - suspend fun revokeToken( + public suspend fun revokeToken( token: String, configure: (HttpRequestBuilder.() -> Unit)? = null ): HttpStatusCode @@ -122,7 +122,7 @@ interface OpenIdConnectClient { * @return [AccessTokenResponse] */ @Throws(OpenIdConnectException::class, CancellationException::class) - suspend fun exchangeToken( + public suspend fun exchangeToken( authCodeRequest: AuthCodeRequest, code: String, configure: (HttpRequestBuilder.() -> Unit)? = null @@ -139,8 +139,7 @@ interface OpenIdConnectClient { * @return [AccessTokenResponse] */ @Throws(OpenIdConnectException::class, CancellationException::class) - @Suppress("Unused") - suspend fun refreshToken( + public suspend fun refreshToken( refreshToken: String, configure: (HttpRequestBuilder.() -> Unit)? = null ): AccessTokenResponse @@ -157,7 +156,7 @@ interface OpenIdConnectClient { */ @Throws(OpenIdConnectException::class, CancellationException::class) @Suppress("MemberVisibilityCanBePrivate") - suspend fun createAccessTokenRequest( + public suspend fun createAccessTokenRequest( authCodeRequest: AuthCodeRequest, code: String, configure: (HttpRequestBuilder.() -> Unit)? = null @@ -175,8 +174,8 @@ interface OpenIdConnectClient { */ @Throws(OpenIdConnectException::class, CancellationException::class) @Suppress("MemberVisibilityCanBePrivate") - suspend fun createRefreshTokenRequest( + public suspend fun createRefreshTokenRequest( refreshToken: String, configure: (HttpRequestBuilder.() -> Unit)? = null ): TokenRequest -} \ No newline at end of file +} diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClientConfig.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClientConfig.kt index b88c8a1d..4248cf7d 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClientConfig.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClientConfig.kt @@ -10,7 +10,7 @@ import kotlin.native.ObjCName * * @return [OpenIdConnectClientConfig] */ -fun OpenIdConnectClientConfig(block: OpenIdConnectClientConfig.() -> Unit): OpenIdConnectClientConfig { +public fun OpenIdConnectClientConfig(block: OpenIdConnectClientConfig.() -> Unit): OpenIdConnectClientConfig { val config = OpenIdConnectClientConfig() config.block() return config @@ -24,51 +24,53 @@ fun OpenIdConnectClientConfig(block: OpenIdConnectClientConfig.() -> Unit): Open @OptIn(ExperimentalObjCName::class) @EndpointMarker @ObjCName(swiftName = "OpenIdConnectClientConfig", name = "OpenIdConnectClientConfig", exact = true) -class OpenIdConnectClientConfig( +@Suppress("LongParameterList") +public class OpenIdConnectClientConfig( /** * If set, no further endpoints have to be configured. * You can override discovered endpoints in [endpoints] */ - val discoveryUri: String? = null, - var endpoints: Endpoints? = null, + public val discoveryUri: String? = null, + public var endpoints: Endpoints? = null, /** * REQUIRED * * [RFC6749](https://datatracker.ietf.org/doc/html/rfc6749#section-2.2) */ - var clientId: String? = null, - var clientSecret: String? = null, + public var clientId: String? = null, + public var clientSecret: String? = null, /** * OPTIONAL * * [RFC6749](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3) */ - var scope: String? = null, + public var scope: String? = null, /** * The Code Challenge Method to use for PKCE. * * Default is [S256][CodeChallengeMethod.S256]). * Set to [off][CodeChallengeMethod.off]) to disable PKCE. */ - var codeChallengeMethod: CodeChallengeMethod = CodeChallengeMethod.S256, + public var codeChallengeMethod: CodeChallengeMethod = CodeChallengeMethod.S256, /** * [rfc6749](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2) */ - var redirectUri: String? = null, + public var redirectUri: String? = null, /** * Url that is used for redirecting back to the app after logout request. - * Is only used if endSession is called on an [EndSessionFlow][org.publicvalue.multiplatform.oidc.flows.EndSessionFlow]. + * Is only used if endSession is called on + * an [EndSessionFlow][org.publicvalue.multiplatform.oidc.flows.EndSessionFlow]. * * [OpenID Spec](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) */ - var postLogoutRedirectUri: String? = null, + public var postLogoutRedirectUri: String? = null, /** * Disables sending nonce in the authentication request. * This is not recommended as [nonce is used to mitigate replay attacks](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes). */ - var disableNonce: Boolean = false + public var disableNonce: Boolean = false ) { /** * Configure the endpoints. @@ -78,7 +80,7 @@ class OpenIdConnectClientConfig( * * [endSessionEndpoint][Endpoints.endSessionEndpoint] is required if you wish to logout. */ - fun endpoints( + public fun endpoints( block: Endpoints.() -> Unit ) { this.endpoints = (endpoints ?: Endpoints()).apply(block) @@ -88,15 +90,15 @@ class OpenIdConnectClientConfig( * Update this client config with discovery document. * Will NOT override already set properties in config. */ - fun updateWithDiscovery(config: OpenIdConnectConfiguration) { + public fun updateWithDiscovery(config: OpenIdConnectConfiguration) { endpoints { - authorizationEndpoint = authorizationEndpoint ?: config.authorization_endpoint - tokenEndpoint = tokenEndpoint ?: config.token_endpoint - endSessionEndpoint = endSessionEndpoint ?: config.end_session_endpoint - userInfoEndpoint = userInfoEndpoint ?: config.userinfo_endpoint - revocationEndpoint = revocationEndpoint ?: config.revocation_endpoint + authorizationEndpoint = authorizationEndpoint ?: config.authorizationEndpoint + tokenEndpoint = tokenEndpoint ?: config.tokenEndpoint + endSessionEndpoint = endSessionEndpoint ?: config.endSessionEndpoint + userInfoEndpoint = userInfoEndpoint ?: config.userinfoEndpoint + revocationEndpoint = revocationEndpoint ?: config.revocationEndpoint } - this.scope = scope ?: config.scopes_supported?.joinToString(" ") + this.scope = scope ?: config.scopesSupported?.joinToString(" ") } } @@ -109,12 +111,12 @@ private annotation class EndpointMarker @OptIn(ExperimentalObjCName::class) @EndpointMarker @ObjCName(swiftName = "Endpoints", name = "Endpoints", exact = true) -data class Endpoints( - var tokenEndpoint: String? = null, - var authorizationEndpoint: String? = null, - var userInfoEndpoint: String? = null, - var endSessionEndpoint: String? = null, - var revocationEndpoint: String? = null +public data class Endpoints( + public var tokenEndpoint: String? = null, + public var authorizationEndpoint: String? = null, + public var userInfoEndpoint: String? = null, + public var endSessionEndpoint: String? = null, + public var revocationEndpoint: String? = null ) { /** * Set a baseUrl that is applied for all endpoints. @@ -122,8 +124,7 @@ data class Endpoints( * tokenEndpoint = "token" * } */ - @Suppress("unused") - fun baseUrl(baseUrl: String, block: Endpoints.() -> Unit) { + public fun baseUrl(baseUrl: String, block: Endpoints.() -> Unit) { val endpoints = Endpoints() endpoints.block() tokenEndpoint = baseUrl + endpoints.tokenEndpoint @@ -139,7 +140,7 @@ data class Endpoints( * @receiver the [OpenIdConnectClientConfig] to validate * @throws OpenIdConnectException if the config is invalid */ -fun OpenIdConnectClientConfig.validate() { +public fun OpenIdConnectClientConfig.validate() { if (discoveryUri.isNullOrBlank()) { if (endpoints?.tokenEndpoint == null) { throw OpenIdConnectException.InvalidUrl("Invalid configuration: tokenEndpoint is null") diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectException.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectException.kt index 47132d44..913d437a 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectException.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectException.kt @@ -1,3 +1,5 @@ +@file:Suppress("ForbiddenPublicDataClass") + package org.publicvalue.multiplatform.oidc import io.ktor.http.HttpStatusCode @@ -7,25 +9,45 @@ import kotlin.native.ObjCName @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "OpenIdConnectException", name = "OpenIdConnectException", exact = true) -sealed class OpenIdConnectException( +public sealed class OpenIdConnectException( override val message: String, override val cause: Throwable? = null -): Exception(message, cause) { +) : Exception(message, cause) { + + public data class InvalidUrl(val url: String?, override val cause: Throwable? = null) : + OpenIdConnectException( + message = "Invalid URL: $url", + cause = cause + ) - data class InvalidUrl(val url: String?, override val cause: Throwable? = null): OpenIdConnectException(message = "Invalid URL: $url", cause = cause) - data class AuthenticationFailure(override val message: String, override val cause: Throwable? = null): OpenIdConnectException(message = "Authentication failed. $message", cause = cause) - data class AuthenticationCancelled(override val message: String = "Authentication cancelled"): OpenIdConnectException(message = "Authentication cancelled", cause = null) - data class UnsuccessfulTokenRequest( + public data class AuthenticationFailure( + override val message: String, + override val cause: Throwable? = null + ) : OpenIdConnectException(message = "Authentication failed. $message", cause = cause) + + public data class AuthenticationCancelled( + override val message: String = "Authentication cancelled" + ) : OpenIdConnectException(message = "Authentication cancelled", cause = null) + + public data class UnsuccessfulTokenRequest( override val message: String, val statusCode: HttpStatusCode, val body: String?, val errorResponse: ErrorResponse?, override val cause: Throwable? = null - ): OpenIdConnectException(message = "Authentication failed. $message", cause = cause) + ) : OpenIdConnectException(message = "Authentication failed. $message", cause = cause) - data class UnsupportedFormat(override val message: String): OpenIdConnectException(message) + public data class UnsupportedFormat(override val message: String) : + OpenIdConnectException(message) - data class TechnicalFailure(override val message: String, override val cause: Throwable?): OpenIdConnectException(message, cause) + public data class TechnicalFailure( + override val message: String, + override val cause: Throwable? + ) : OpenIdConnectException( + message, + cause + ) - data class InvalidConfiguration(override val message: String): OpenIdConnectException(message) + public data class InvalidConfiguration(override val message: String) : + OpenIdConnectException(message) } diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/RunCatchingWrapException.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/RunCatchingWrapException.kt index edd50ea1..839b7f6c 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/RunCatchingWrapException.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/RunCatchingWrapException.kt @@ -1,6 +1,6 @@ package org.publicvalue.multiplatform.oidc -inline fun wrapExceptions(block: () -> R): R { +public inline fun wrapExceptions(block: () -> R): R { return try { block() } catch (e: OpenIdConnectException) { @@ -8,4 +8,4 @@ inline fun wrapExceptions(block: () -> R): R { } catch (e: Throwable) { throw OpenIdConnectException.TechnicalFailure(e.message ?: "Unknown error", e) } -} \ No newline at end of file +} diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/discovery/OpenIdConnectDiscover.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/discovery/OpenIdConnectDiscover.kt index 4f3c1ae3..c8db6307 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/discovery/OpenIdConnectDiscover.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/discovery/OpenIdConnectDiscover.kt @@ -19,7 +19,7 @@ import kotlin.native.ObjCName */ @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "OpenIdConnectDiscover", name = "OpenIdConnectDiscover", exact = true) -class OpenIdConnectDiscover( +public class OpenIdConnectDiscover( private val httpClient: HttpClient = HttpClient() ) { @@ -36,7 +36,10 @@ class OpenIdConnectDiscover( * @param configure configuration closure to configure the http request builder with * @return [OpenIdConnectConfiguration] */ - suspend fun downloadConfiguration(configurationUrl: String, configure: (HttpRequestBuilder.() -> Unit)? = null): OpenIdConnectConfiguration { + public suspend fun downloadConfiguration( + configurationUrl: String, + configure: (HttpRequestBuilder.() -> Unit)? = null + ): OpenIdConnectConfiguration { return downloadConfiguration(Url(configurationUrl), configure) } @@ -47,7 +50,10 @@ class OpenIdConnectDiscover( * @param configure configuration closure to configure the http request builder with * @return [OpenIdConnectConfiguration] */ - suspend fun downloadConfiguration(configurationUrl: Url, configure: (HttpRequestBuilder.() -> Unit)? = null): OpenIdConnectConfiguration { + public suspend fun downloadConfiguration( + configurationUrl: Url, + configure: (HttpRequestBuilder.() -> Unit)? = null + ): OpenIdConnectConfiguration { val result = httpClient.get(configurationUrl) { configure?.invoke(this) } @@ -56,10 +62,10 @@ class OpenIdConnectDiscover( } } -private suspend inline fun HttpResponse.forceUnwrapBody(json: Json = Json): T = +private suspend inline fun HttpResponse.forceUnwrapBody(json: Json = Json): T = if (call.response.status.isSuccess()) { - val bodyString:String = call.body() + val bodyString: String = call.body() json.decodeFromString(bodyString) } else { throw Exception("Could not download discovery document: $this") - } \ No newline at end of file + } diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/AuthCodeResult.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/AuthCodeResult.kt index 1e9193c9..8d2580c5 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/AuthCodeResult.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/AuthCodeResult.kt @@ -4,8 +4,8 @@ import kotlinx.serialization.Serializable import kotlin.experimental.ExperimentalObjCName import kotlin.native.ObjCName -typealias AuthCodeResponse = Result -typealias EndSessionResponse = Result +public typealias AuthCodeResponse = Result +public typealias EndSessionResponse = Result /** * Result of an Auth Code Request @@ -13,7 +13,7 @@ typealias EndSessionResponse = Result @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "AuthCodeResult", name = "AuthCodeResult", exact = true) @Serializable -data class AuthCodeResult( +public data class AuthCodeResult( val code: String?, val state: String? -) \ No newline at end of file +) diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/CodeAuthFlow.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/CodeAuthFlow.kt index 48ca4ac3..12e69654 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/CodeAuthFlow.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/CodeAuthFlow.kt @@ -23,16 +23,15 @@ import kotlin.native.ObjCName */ @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "AbstractCodeAuthFlow", name = "AbstractCodeAuthFlow", exact = true) -interface CodeAuthFlow { - val client: OpenIdConnectClient +public interface CodeAuthFlow { + public val client: OpenIdConnectClient /** * For some reason the default parameter is not available in Platform implementations, * so this provides an empty parameter method instead. */ - @Suppress("unused") @Throws(CancellationException::class, OpenIdConnectException::class) - suspend fun getAccessToken(): AccessTokenResponse = getAccessToken(null, null) + public suspend fun getAccessToken(): AccessTokenResponse = getAccessToken(null, null) /** * Start the authorization flow to request an access token. @@ -40,13 +39,17 @@ interface CodeAuthFlow { * @param configure configuration closure to configure the http request builder with (will _not_ * be used for discovery if necessary) */ - @Suppress("unused") @Throws(CancellationException::class, OpenIdConnectException::class) @Deprecated( message = "Use getAccessToken(configureAuthUrl, configureTokenExchange) instead", replaceWith = ReplaceWith("getAccessToken(configureAuthUrl = null, configureTokenExchange = configure)") ) - suspend fun getAccessToken(configure: (HttpRequestBuilder.() -> Unit)? = null): AccessTokenResponse = getAccessToken(null, configure) + public suspend fun getAccessToken( + configure: (HttpRequestBuilder.() -> Unit)? = null + ): AccessTokenResponse = getAccessToken( + null, + configure + ) /** * Start the authorization flow to request an access token. @@ -55,9 +58,8 @@ interface CodeAuthFlow { * @param configureTokenExchange configuration closure to configure the http request builder with (will _not_ * be used for discovery if necessary) */ - @Suppress("unused") @Throws(CancellationException::class, OpenIdConnectException::class) - suspend fun getAccessToken( + public suspend fun getAccessToken( configureAuthUrl: (URLBuilder.() -> Unit)? = null, configureTokenExchange: (HttpRequestBuilder.() -> Unit)? = null ): AccessTokenResponse = wrapExceptions { @@ -68,12 +70,18 @@ interface CodeAuthFlow { return getAccessToken(request, configureTokenExchange) } - private suspend fun getAccessToken(request: AuthCodeRequest, configure: (HttpRequestBuilder.() -> Unit)?): AccessTokenResponse { + private suspend fun getAccessToken( + request: AuthCodeRequest, + configure: (HttpRequestBuilder.() -> Unit)? + ): AccessTokenResponse { val codeResponse = getAuthorizationCode(request) return codeResponse.fold( onSuccess = { exchangeToken( - client = client, request = request, result = it, configure = configure + client = client, + request = request, + result = it, + configure = configure ) }, onFailure = { @@ -88,7 +96,7 @@ interface CodeAuthFlow { * @return the Authorization Code. */ @Throws(CancellationException::class, OpenIdConnectException::class) - abstract suspend fun getAuthorizationCode(request: AuthCodeRequest): AuthCodeResponse + public suspend fun getAuthorizationCode(request: AuthCodeRequest): AuthCodeResponse private suspend fun exchangeToken( client: OpenIdConnectClient, @@ -96,18 +104,18 @@ interface CodeAuthFlow { result: AuthCodeResult, configure: (HttpRequestBuilder.() -> Unit)? ): AccessTokenResponse { - if (result.code != null) { - if (!request.validateState(result.state ?: "")) { - throw OpenIdConnectException.AuthenticationFailure("Invalid state") - } - val response = client.exchangeToken(request, result.code, configure) - val nonce = response.id_token?.parseJwt()?.payload?.nonce - if (!request.validateNonce(nonce ?: "")) { - throw OpenIdConnectException.AuthenticationFailure("Invalid nonce") - } - return response - } else { - throw OpenIdConnectException.AuthenticationFailure("No auth code", cause = null) + if (result.code != null) { + if (!request.validateState(result.state ?: "")) { + throw OpenIdConnectException.AuthenticationFailure("Invalid state") + } + val response = client.exchangeToken(request, result.code, configure) + val nonce = response.idToken?.parseJwt()?.payload?.nonce + if (!request.validateNonce(nonce ?: "")) { + throw OpenIdConnectException.AuthenticationFailure("Invalid nonce") } + return response + } else { + throw OpenIdConnectException.AuthenticationFailure("No auth code", cause = null) + } } -} \ No newline at end of file +} diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/EndSessionFlow.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/EndSessionFlow.kt index fab83568..599c22b7 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/EndSessionFlow.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/EndSessionFlow.kt @@ -12,8 +12,8 @@ import kotlin.coroutines.cancellation.CancellationException * Uses the [postLogoutRedirectUri][org.publicvalue.multiplatform.oidc.OpenIdConnectClientConfig.postLogoutRedirectUri] * to request redirection after logout to return to the app. */ -interface EndSessionFlow { - val client: OpenIdConnectClient +public interface EndSessionFlow { + public val client: OpenIdConnectClient /** * End session using a GET-Request in a WebView. @@ -23,10 +23,10 @@ interface EndSessionFlow { * @param configureEndSessionUrl configuration closure to configure the http request builder with */ @Throws(CancellationException::class, OpenIdConnectException::class) - suspend fun endSession( + public suspend fun endSession( idToken: String?, configureEndSessionUrl: (URLBuilder.() -> Unit)? = null, - ) = wrapExceptions { + ): EndSessionResponse = wrapExceptions { if (!client.config.discoveryUri.isNullOrEmpty()) { client.discover() } @@ -35,5 +35,5 @@ interface EndSessionFlow { } @Throws(CancellationException::class, OpenIdConnectException::class) - suspend fun endSession(request: EndSessionRequest): EndSessionResponse -} \ No newline at end of file + public suspend fun endSession(request: EndSessionRequest): EndSessionResponse +} diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/Pkce.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/Pkce.kt index bc4e7222..412999ba 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/Pkce.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/flows/Pkce.kt @@ -12,12 +12,12 @@ import kotlin.native.ObjCName */ @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "PKCE", name = "PKCE", exact = true) -class Pkce( +public class Pkce( codeChallengeMethod: CodeChallengeMethod, /** For token request **/ - val codeVerifier: String = verifier(), + public val codeVerifier: String = verifier(), /** For authorization **/ - val codeChallenge: String = challenge(codeVerifier, codeChallengeMethod), + public val codeChallenge: String = challenge(codeVerifier, codeChallengeMethod), ) { private companion object { fun verifier(): String { @@ -30,4 +30,3 @@ class Pkce( } } } - diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/AuthCodeRequest.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/AuthCodeRequest.kt index b9fdbde2..8191d6c7 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/AuthCodeRequest.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/AuthCodeRequest.kt @@ -8,18 +8,18 @@ import kotlin.native.ObjCName @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "AuthCodeRequest", name = "AuthCodeRequest", exact = true) -data class AuthCodeRequest( - val url: Url, - val config: OpenIdConnectClientConfig, - val pkce: Pkce, - val state: String, - val nonce: String? +public data class AuthCodeRequest( + public val url: Url, + public val config: OpenIdConnectClientConfig, + public val pkce: Pkce, + public val state: String, + public val nonce: String? ) -fun AuthCodeRequest.validateState(state: String): Boolean { +public fun AuthCodeRequest.validateState(state: String): Boolean { return state == this.state } -fun AuthCodeRequest.validateNonce(nonce: String): Boolean { +public fun AuthCodeRequest.validateNonce(nonce: String): Boolean { return config.disableNonce || nonce == this.nonce -} \ No newline at end of file +} diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/CodeChallengeMethod.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/CodeChallengeMethod.kt index 2f1570c5..12f0deaf 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/CodeChallengeMethod.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/CodeChallengeMethod.kt @@ -8,22 +8,21 @@ import kotlin.native.ObjCName */ @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "CodeChallengeMethod", name = "CodeChallengeMethod", exact = true) -enum class CodeChallengeMethod( - val queryString: String? +public enum class CodeChallengeMethod( + public val queryString: String? ) { /** Send a random code_challenge in code request and a SHA-256 hashed code_verifier in token * request. - **/ + **/ S256("S256"), /** * code_challenge = code_verifier */ - @Suppress("unused") - plain("plain"), + PLAIN("plain"), /** * Disable PKCE Headers. */ - off(null) -} \ No newline at end of file + OFF(null) +} diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/EndSessionRequest.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/EndSessionRequest.kt index a96ad44f..355728ff 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/EndSessionRequest.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/EndSessionRequest.kt @@ -6,6 +6,6 @@ import kotlin.native.ObjCName @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "EndSessionRequest", name = "EndSessionRequest", exact = true) -data class EndSessionRequest( - val url: Url -) \ No newline at end of file +public data class EndSessionRequest( + public val url: Url +) diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/IdToken.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/IdToken.kt index 038d6f5e..5a25017f 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/IdToken.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/IdToken.kt @@ -8,7 +8,7 @@ import kotlin.native.ObjCName */ @OptIn(ExperimentalObjCName::class) @ObjCName("IdToken", "IdToken", exact = true) -data class IdToken( +public data class IdToken( /** Required: Issuer (must match the discovery document) **/ val iss: String?, /** Required: Subject identifier **/ @@ -20,7 +20,7 @@ data class IdToken( /** Required: Issued at **/ val iat: Long?, /** Optional time of user auth **/ - val auth_time: Long?, + val authTime: Long?, /** Optional, if present, must match request nonce **/ val nonce: String?, /** Optional: Authentication Context Class Reference **/ @@ -34,7 +34,7 @@ data class IdToken( val alg: String?, val kid: String?, /** Optional: Access Token hash **/ - val at_hash: String?, + val atHash: String?, val additionalClaims: Map -) \ No newline at end of file +) diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/Jwt.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/Jwt.kt index e6fca40e..1b222f48 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/Jwt.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/Jwt.kt @@ -28,24 +28,28 @@ private val json by lazy { @OptIn(ExperimentalObjCName::class) @ObjCName("Jwt", "Jwt", exact = true) -data class Jwt( - val header: JwtHeader, - val payload: IdToken, - val signature: String? +public data class Jwt( + public val header: JwtHeader, + public val payload: IdToken, + public val signature: String? ) { - companion object { + public companion object { /** - * JWTs are either encoded using JWS Compact Serialization (signed, 3 parts) or JWE Compact Serialization (encrypted, 5 parts). + * JWTs are either encoded using JWS Compact Serialization (signed, 3 parts) + * or JWE Compact Serialization (encrypted, 5 parts). * We only support JWS Compact Serialization. */ @OptIn(ExperimentalObjCRefinement::class) @Throws(OpenIdConnectException::class) @HiddenFromObjC - fun parse(string: String): Jwt { + public fun parse(string: String): Jwt { val parts = string.split('.') + @Suppress("MagicNumber") if (parts.size > 3) { - throw OpenIdConnectException.UnsupportedFormat("Expected at most 3 JWT token parts (this may be an encrypted token which is unsupported)") + throw OpenIdConnectException.UnsupportedFormat( + "Expected at most 3 JWT token parts (this may be an encrypted token which is unsupported)" + ) } else if (parts.size < 2) { throw OpenIdConnectException.UnsupportedFormat("Expected at least 2 JWT token parts") } @@ -71,23 +75,23 @@ data class Jwt( @OptIn(ExperimentalObjCName::class) @Serializable @ObjCName("JwtHeader", "JwtHeader", exact = true) -data class JwtHeader( +public data class JwtHeader( /** Required: Algorithm. Possible values: https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 **/ - val alg: String, - val jku: String?, - val jwk: String?, - val kid: String?, - val x5u: String?, - val x5c: String?, - val x5t: String?, + public val alg: String, + public val jku: String?, + public val jwk: String?, + public val kid: String?, + public val x5u: String?, + public val x5c: String?, + public val x5t: String?, @SerialName("x5t#S256") - val x5tS256: String?, - val typ: String?, - val cty: String?, - val crit: String? + public val x5tS256: String?, + public val typ: String?, + public val cty: String?, + public val crit: String? ) { - companion object { - fun parse(string: String): JwtHeader { + public companion object { + public fun parse(string: String): JwtHeader { return json.decodeFromString(string) } } @@ -96,12 +100,12 @@ data class JwtHeader( @OptIn(ExperimentalObjCName::class) @JvmInline @ObjCName("JwtClaims", "JwtClaims", exact = true) -value class JwtClaims( - val claims: Map +public value class JwtClaims( + public val claims: Map ) { - companion object { + public companion object { @Throws(OpenIdConnectException::class) - fun parse(string: String): JwtClaims { + public fun parse(string: String): JwtClaims { try { val map = json.decodeFromString>(string) .map { entry -> @@ -117,9 +121,9 @@ value class JwtClaims( private fun JsonElement.toKotlin( key: String - ): Any? = when(this) { - is JsonArray -> this.mapIndexed { index, it -> - it.toKotlin("$key-$index") + ): Any? = when (this) { + is JsonArray -> this.mapIndexed { index, jsonElement -> + jsonElement.toKotlin("$key-$index") } is JsonObject -> this.map { (key, value) -> @@ -148,14 +152,14 @@ private fun JwtClaims.toOpenIdConnectToken(): IdToken = aud = claims["aud"]?.parseListOrString(), exp = claims["exp"] as Long?, iat = claims["iat"] as Long?, - auth_time = claims["auth_time"] as Long?, + authTime = claims["auth_time"] as Long?, nonce = claims["nonce"] as String?, acr = claims["acr"] as String?, amr = claims["amr"]?.parseListOrString(), azp = claims["azp"] as String?, alg = claims["alg"] as String?, kid = claims["kid"] as String?, - at_hash = claims["at_hash"] as String?, + atHash = claims["at_hash"] as String?, additionalClaims = claims ) @@ -169,4 +173,4 @@ private fun Any.parseListOrString() = // this is visible from swift as JwtKt.parse() @Throws(OpenIdConnectException::class) -fun String.parseJwt() = Jwt.parse(this) +public fun String.parseJwt(): Jwt = Jwt.parse(this) diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/JwtConvenienceExtensions.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/JwtConvenienceExtensions.kt index fc47883e..8782c33b 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/JwtConvenienceExtensions.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/JwtConvenienceExtensions.kt @@ -1,4 +1,3 @@ -@file:Suppress("Unused") package org.publicvalue.multiplatform.oidc.types import org.publicvalue.multiplatform.oidc.OpenIdConnectException @@ -9,11 +8,11 @@ import kotlin.native.ObjCName @OptIn(ExperimentalObjCName::class) @ObjCName("JwtParser", "JwtParser", exact = true) -object JwtParser { +public object JwtParser { /** * Objective-C convenience function * @see Jwt.parse */ @Throws(OpenIdConnectException::class) - fun parse(from: String) = Jwt.parse(from) -} \ No newline at end of file + public fun parse(from: String): Jwt = Jwt.parse(from) +} diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/TokenRequest.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/TokenRequest.kt index 5d2daf99..e5346f03 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/TokenRequest.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/TokenRequest.kt @@ -7,7 +7,7 @@ import kotlin.native.ObjCName @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "TokenRequest", name = "TokenRequest", exact = true) -data class TokenRequest( - val request: HttpStatement, - val formParameters: Parameters -) \ No newline at end of file +public data class TokenRequest( + public val request: HttpStatement, + public val formParameters: Parameters +) diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/AccessTokenResponse.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/AccessTokenResponse.kt index 6a27feec..0f1e862f 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/AccessTokenResponse.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/AccessTokenResponse.kt @@ -1,5 +1,6 @@ package org.publicvalue.multiplatform.oidc.types.remote +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlin.experimental.ExperimentalObjCName import kotlin.native.ObjCName @@ -15,61 +16,70 @@ import kotlin.time.ExperimentalTime @OptIn(ExperimentalObjCName::class, ExperimentalTime::class) @Serializable @ObjCName(swiftName = "AccessTokenResponse", name = "AccessTokenResponse", exact = true) -data class AccessTokenResponse constructor( +public data class AccessTokenResponse( /** * **Required** * * The access token issued by the authorization server. */ - val access_token: String, + @SerialName("access_token") + val accessToken: String, /** * **Required** * - * The value MUST be _Bearer_ or another token_type value that the Client has negotiated with the Authorization Server. + * The value MUST be _Bearer_ or another token_type value + * that the Client has negotiated with the Authorization Server. * - * Clients implementing this profile MUST support the OAuth 2.0 Bearer Token Usage [RFC6750](https://openid.net/specs/openid-connect-core-1_0.html#RFC6750) specification. + * Clients implementing this profile MUST support the OAuth 2.0 Bearer Token Usage + * [RFC6750](https://openid.net/specs/openid-connect-core-1_0.html#RFC6750) specification. */ - val token_type: String? = null, + @SerialName("token_type") + val tokenType: String? = null, /** * **Recommended** * - * The lifetime in seconds of the [access_token]. + * The lifetime in seconds of the [accessToken]. * For example, the value "3600" denotes that the access token will * expire in one hour from the time the response was generated. * * If omitted, the authorization server SHOULD provide the * expiration time via other means or document the default value. */ - val expires_in: Int? = null, + @SerialName("expires_in") + val expiresIn: Int? = null, /** * **Optional** * * The refresh token, which can be used to obtain new access tokens using the same authorization grant type. */ - val refresh_token: String? = null, + @SerialName("refresh_token") + val refreshToken: String? = null, /** * **Not specified in OAuth 2.0** * - * The lifetime in seconds of the [refresh_token]. + * The lifetime in seconds of the [refreshToken]. * For example, the value "3600" denotes that the refresh token will * expire in one hour from the time the response was generated. */ - val refresh_token_expires_in: Int? = null, + @SerialName("refresh_token_expires_in") + val refreshTokenExpiresIn: Int? = null, /** * **Required in OpenIDConnect** * * ID Token value associated with the authenticated session. */ - val id_token: String? = null, + @SerialName("id_token") + val idToken: String? = null, /** Optional if identical to request **/ val scope: String? = null, /** Computed locally **/ - val received_at: Long = Clock.System.now().epochSeconds //.System.now().epochSeconds + @SerialName("received_at") + val receivedAt: Long = Clock.System.now().epochSeconds // .System.now().epochSeconds ) diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/ErrorResponse.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/ErrorResponse.kt index 94a30d51..35c97c37 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/ErrorResponse.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/ErrorResponse.kt @@ -1,5 +1,6 @@ package org.publicvalue.multiplatform.oidc.types.remote +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlin.experimental.ExperimentalObjCName import kotlin.native.ObjCName @@ -12,24 +13,47 @@ import kotlin.native.ObjCName @OptIn(ExperimentalObjCName::class) @ObjCName(swiftName = "OpenIdConnectErrorResponse", name = "OpenIdConnectErrorResponse", exact = true) @Serializable -data class ErrorResponse( +public data class ErrorResponse( val error: Error, - val error_description: String?, - val error_uri: String?, + @SerialName("error_description") + val errorDescription: String?, + @SerialName("error_uri") + val errorUri: String?, val state: String? ) { @Serializable - enum class Error { - invalid_client, - invalid_grant, - bad_verification_code, - invalid_request, - unauthorized_client, - unsupported_grant_type, - access_denied, - unsupported_response_type, - invalid_scope, - server_error, - temporarily_unavailable + public enum class Error { + @SerialName("invalid_client") + INVALID_CLIENT, + + @SerialName("invalid_grant") + INVALID_GRANT, + + @SerialName("bad_verification_code") + BAD_VERIFICATION_CODE, + + @SerialName("invalid_request") + INVALID_REQUEST, + + @SerialName("unauthorized_client") + UNAUTHORIZED_CLIENT, + + @SerialName("unsupported_grant_type") + UNSUPPORTED_GRANT_TYPE, + + @SerialName("access_denied") + ACCESS_DENIED, + + @SerialName("unsupported_response_type") + UNSUPPORTED_RESPONSE_TYPE, + + @SerialName("invalid_scope") + INVALID_SCOPE, + + @SerialName("server_error") + SERVER_ERROR, + + @SerialName("temporarily_unavailable") + TEMPORARILY_UNAVAILABLE } } diff --git a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/OpenIdConnectConfiguration.kt b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/OpenIdConnectConfiguration.kt index 7fe33e41..5e555dbf 100644 --- a/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/OpenIdConnectConfiguration.kt +++ b/oidc-core/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/types/remote/OpenIdConnectConfiguration.kt @@ -1,5 +1,6 @@ package org.publicvalue.multiplatform.oidc.types.remote +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlin.experimental.ExperimentalObjCName import kotlin.native.ObjCName @@ -10,24 +11,41 @@ import kotlin.native.ObjCName @OptIn(ExperimentalObjCName::class) @Serializable @ObjCName(swiftName = "OpenIdConnectConfiguration") -data class OpenIdConnectConfiguration( - val authorization_endpoint: String? = null, - val token_endpoint: String? = null, - val device_authorization_endpoint: String? = null, - val userinfo_endpoint: String? = null, - val end_session_endpoint: String? = null, - val introspection_endpoint: String? = null, - val revocation_endpoint: String? = null, +public data class OpenIdConnectConfiguration( + @SerialName("authorization_endpoint") + val authorizationEndpoint: String? = null, + @SerialName("token_endpoint") + val tokenEndpoint: String? = null, + @SerialName("device_authorization_endpoint") + val deviceAuthorizationEndpoint: String? = null, + @SerialName("userinfo_endpoint") + val userinfoEndpoint: String? = null, + @SerialName("end_session_endpoint") + val endSessionEndpoint: String? = null, + @SerialName("introspection_endpoint") + val introspectionEndpoint: String? = null, + @SerialName("revocation_endpoint") + val revocationEndpoint: String? = null, val issuer: String? = null, - val jwks_uri: String? = null, - val response_types_supported: List? = null, - val id_token_signing_alg_values_supported: List? = null, - val frontchannel_logout_supported: Boolean? = null, - val scopes_supported: List? = null, - val claims_supported: List? = null, - val subject_types_supported: List? = null, - val token_endpoint_auth_methods_supported: List? = null, - val grant_types_supported: List? = null, - val introspection_endpoint_auth_methods_supported: List? = null, -) \ No newline at end of file + @SerialName("jwks_uri") + val jwksUri: String? = null, + @SerialName("response_types_supported") + val responseTypesSupported: List? = null, + @SerialName("id_token_signing_alg_values_supported") + val idTokenSigningAlgValuesSupported: List? = null, + @SerialName("frontchannel_logout_supported") + val frontChannelLogoutSupported: Boolean? = null, + @SerialName("scopes_supported") + val scopesSupported: List? = null, + @SerialName("claims_supported") + val claimsSupported: List? = null, + @SerialName("subject_types_supported") + val subjectTypesSupported: List? = null, + @SerialName("token_endpoint_auth_methods_supported") + val tokenEndpointAuthMethodsSupported: List? = null, + @SerialName("grant_types_supported") + val grantTypesSupported: List? = null, + @SerialName("introspection_endpoint_auth_methods_supported") + val introspectionEndpointAuthMethodsSupported: List? = null, +) diff --git a/oidc-core/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/JwtTest.kt b/oidc-core/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/JwtTest.kt index d5c517ed..158569cc 100644 --- a/oidc-core/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/JwtTest.kt +++ b/oidc-core/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/JwtTest.kt @@ -11,9 +11,10 @@ import kotlin.test.Test class JwtTest { @Test + @Suppress("MaximumLineLength") fun test() { - val id_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg" - val jwt = Jwt.parse(id_token) + val idToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg" + val jwt = Jwt.parse(idToken) assertThat(jwt.header.alg).isEqualTo("RS256") assertThat(jwt.header.kid).isEqualTo("1e9gdk7") @@ -26,11 +27,13 @@ class JwtTest { } @Test + @Suppress("MaximumLineLength") fun brokenToken() { var e: Exception? = null try { - val id_token = "eyJhxbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg" - Jwt.parse(id_token) + @Suppress("MaximumLineLength") + val idToken = "eyJhxbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg" + Jwt.parse(idToken) } catch (ex: Exception) { e = ex } @@ -38,7 +41,9 @@ class JwtTest { } @Test + @Suppress("MaximumLineLength") fun multipleAudiences() { + @Suppress("MaximumLineLength") val idToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3MDE2OTQ3MDYsImV4cCI6MTczMzIzMDcwNiwiYXVkIjpbInd3dy5leGFtcGxlLmNvbSIsInNlY29uZCBhdWRpZW5jZSJdLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiRW1haWwiOiJiZWVAZXhhbXBsZS5jb20ifQ.k140fPrDnsTsBUPwkIs8g4Hfu-DTKwNsv8KDkowOu9g" val jwt = Jwt.parse(idToken) @@ -47,6 +52,7 @@ class JwtTest { @Test fun multipleAmr() { + @Suppress("MaximumLineLength") val idToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3MTg2MTczNzIsImV4cCI6MTc1MDE1MzM3MiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsImFtciI6WyJzbXMiLCJtZmEiLCJwd2QiXX0.t86lfvm-eV3c2JJP4WQLtDGXm3rWSCKy_MD2colIzLA" val jwt = Jwt.parse(idToken) @@ -55,16 +61,20 @@ class JwtTest { @Test fun additionalClaims() { + @Suppress("MaximumLineLength") val idToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3MDE2OTQ3MDYsImV4cCI6MTczMzIzMDcwNiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkVtYWlsIjoiYmVlQGV4YW1wbGUuY29tIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiTWFuYWdlciIsImZhbWlseV9uYW1lIjoiU29tZW9uZSJ9.mjk1il9Nrr5JouTZ9kRcWb1R84bw30vdrYaw0CVsv9A" val jwt = Jwt.parse(idToken) assertThat(jwt.payload.additionalClaims.get("Email")).isEqualTo("bee@example.com") - assertThat(jwt.payload.additionalClaims.get("http://schemas.microsoft.com/ws/2008/06/identity/claims/role")).isEqualTo("Manager") + assertThat( + jwt.payload.additionalClaims.get("http://schemas.microsoft.com/ws/2008/06/identity/claims/role") + ).isEqualTo("Manager") assertThat(jwt.payload.additionalClaims.get("family_name")).isEqualTo("Someone") } @Test fun jsonObjectsInClaims() { + @Suppress("MaximumLineLength") val idToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ewogICAgInN1YiI6ICJhYWFhYWFhYS1iYmJiLWNjY2MtZGRkZC1lZWVlZWVlZWVlZWUiLAogICAgImNvZ25pdG86Z3JvdXBzIjogWwogICAgICAgICJ0ZXN0LWdyb3VwLWEiLAogICAgICAgICJ0ZXN0LWdyb3VwLWIiLAogICAgICAgICJ0ZXN0LWdyb3VwLWMiCiAgICBdLAogICAgImVtYWlsX3ZlcmlmaWVkIjogdHJ1ZSwKICAgICJjb2duaXRvOnByZWZlcnJlZF9yb2xlIjogImFybjphd3M6aWFtOjoxMTExMjIyMjMzMzM6cm9sZS9teS10ZXN0LXJvbGUiLAogICAgImlzcyI6ICJodHRwczovL2NvZ25pdG8taWRwLnVzLXdlc3QtMi5hbWF6b25hd3MuY29tL3VzLXdlc3QtMl9leGFtcGxlIiwKICAgICJjb2duaXRvOnVzZXJuYW1lIjogIm15LXRlc3QtdXNlciIsCiAgICAibWlkZGxlX25hbWUiOiAiSmFuZSIsCiAgICAibm9uY2UiOiAiYWJjZGVmZyIsCiAgICAib3JpZ2luX2p0aSI6ICJhYWFhYWFhYS1iYmJiLWNjY2MtZGRkZC1lZWVlZWVlZWVlZWUiLAogICAgImNvZ25pdG86cm9sZXMiOiBbCiAgICAgICAgImFybjphd3M6aWFtOjoxMTExMjIyMjMzMzM6cm9sZS9teS10ZXN0LXJvbGUiCiAgICBdLAogICAgImF1ZCI6ICJ4eHh4eHh4eHh4eHhleGFtcGxlIiwKICAgICJpZGVudGl0aWVzIjogWwogICAgICAgIHsKICAgICAgICAgICAgInVzZXJJZCI6ICJhbXpuMS5hY2NvdW50LkVYQU1QTEUiLAogICAgICAgICAgICAicHJvdmlkZXJOYW1lIjogIkxvZ2luV2l0aEFtYXpvbiIsCiAgICAgICAgICAgICJwcm92aWRlclR5cGUiOiAiTG9naW5XaXRoQW1hem9uIiwKICAgICAgICAgICAgImlzc3VlciI6IG51bGwsCiAgICAgICAgICAgICJwcmltYXJ5IjogInRydWUiLAogICAgICAgICAgICAiZGF0ZUNyZWF0ZWQiOiAiMTY0MjY5OTExNzI3MyIKICAgICAgICB9CiAgICBdLAogICAgImV2ZW50X2lkIjogIjY0ZjUxM2JlLTMyZGItNDJiMC1iNzhlLWIwMjEyN2I0ZjQ2MyIsCiAgICAidG9rZW5fdXNlIjogImlkIiwKICAgICJhdXRoX3RpbWUiOiAxNjc2MzEyNzc3LAogICAgImV4cCI6IDE2NzYzMTYzNzcsCiAgICAiaWF0IjogMTY3NjMxMjc3NywKICAgICJqdGkiOiAiYWFhYWFhYWEtYmJiYi1jY2NjLWRkZGQtZWVlZWVlZWVlZWVlIiwKICAgICJlbWFpbCI6ICJteS10ZXN0LXVzZXJAZXhhbXBsZS5jb20iCn0K.mjk1il9Nrr5JouTZ9kRcWb1R84bw30vdrYaw0CVsv9A" val jwt = Jwt.parse(idToken) @@ -78,4 +88,4 @@ class JwtTest { assertThat(identitiesMap["userId"]).isEqualTo("amzn1.account.EXAMPLE") assertThat(identitiesMap["providerName"]).isEqualTo("LoginWithAmazon") } -} \ No newline at end of file +} diff --git a/oidc-core/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClientConvenienceExtensions.kt b/oidc-core/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClientConvenienceExtensions.kt index b9af6bc7..7e5033ae 100644 --- a/oidc-core/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClientConvenienceExtensions.kt +++ b/oidc-core/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/OpenIdConnectClientConvenienceExtensions.kt @@ -1,14 +1,18 @@ @file:Suppress("Unused") + package org.publicvalue.multiplatform.oidc import io.ktor.client.HttpClientConfig import io.ktor.client.plugins.api.createClientPlugin import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpStatusCode import org.publicvalue.multiplatform.oidc.types.AuthCodeRequest +import org.publicvalue.multiplatform.oidc.types.TokenRequest +import org.publicvalue.multiplatform.oidc.types.remote.AccessTokenResponse import kotlin.coroutines.cancellation.CancellationException -// swift convenience overloads with default parameters for suspend functions +// swift convenience overloads with default parameters for public suspend functions // defined on DefaultOpenIdConnectClient because we cannot export extensions for interfaces yet /** @@ -17,7 +21,7 @@ import kotlin.coroutines.cancellation.CancellationException * @suppress */ @Throws(OpenIdConnectException::class, CancellationException::class) -suspend fun DefaultOpenIdConnectClient.discover() = +public suspend fun DefaultOpenIdConnectClient.discover(): Unit = discover(null) /** @@ -26,7 +30,10 @@ suspend fun DefaultOpenIdConnectClient.discover() = * @suppress */ @Throws(OpenIdConnectException::class, CancellationException::class) -suspend fun DefaultOpenIdConnectClient.exchangeToken(authCodeRequest: AuthCodeRequest, code: String) = +public suspend fun DefaultOpenIdConnectClient.exchangeToken( + authCodeRequest: AuthCodeRequest, + code: String +): AccessTokenResponse = exchangeToken(authCodeRequest, code, null) /** @@ -35,7 +42,7 @@ suspend fun DefaultOpenIdConnectClient.exchangeToken(authCodeRequest: AuthCodeRe * @suppress */ @Throws(OpenIdConnectException::class, CancellationException::class) -suspend fun DefaultOpenIdConnectClient.endSession(idToken: String) = +public suspend fun DefaultOpenIdConnectClient.endSession(idToken: String): HttpStatusCode = endSession(idToken, null) /** @@ -44,7 +51,10 @@ suspend fun DefaultOpenIdConnectClient.endSession(idToken: String) = * @suppress */ @Throws(OpenIdConnectException::class, CancellationException::class) -suspend fun DefaultOpenIdConnectClient.createAccessTokenRequest(authCodeRequest: AuthCodeRequest, code: String) = +public suspend fun DefaultOpenIdConnectClient.createAccessTokenRequest( + authCodeRequest: AuthCodeRequest, + code: String +): TokenRequest = createAccessTokenRequest(authCodeRequest, code, null) /** @@ -53,7 +63,7 @@ suspend fun DefaultOpenIdConnectClient.createAccessTokenRequest(authCodeRequest: * @suppress */ @Throws(OpenIdConnectException::class, CancellationException::class) -suspend fun DefaultOpenIdConnectClient.createRefreshTokenRequest(refreshToken: String) = +public suspend fun DefaultOpenIdConnectClient.createRefreshTokenRequest(refreshToken: String): TokenRequest = createRefreshTokenRequest(refreshToken, null) /** @@ -62,19 +72,22 @@ suspend fun DefaultOpenIdConnectClient.createRefreshTokenRequest(refreshToken: S * @suppress */ @Throws(OpenIdConnectException::class, CancellationException::class) -suspend fun DefaultOpenIdConnectClient.refreshToken(refreshToken: String) = +public suspend fun DefaultOpenIdConnectClient.refreshToken(refreshToken: String): AccessTokenResponse = refreshToken(refreshToken, null) /** * Create and install a custom client plugin. * @suppress */ -fun HttpClientConfig<*>.installClientPlugin(name: String, - onRequest: (HttpRequestBuilder, Any) -> Unit = {_,_ ->}, - onResponse: (HttpResponse) -> Unit = {}, - onClose: () -> Unit = {}, +public fun HttpClientConfig<*>.installClientPlugin( + name: String, + onRequest: (HttpRequestBuilder, Any) -> Unit = { _, _ -> }, + onResponse: (HttpResponse) -> Unit = {}, + onClose: () -> Unit = {}, ) { - val plugin = createClientPlugin(name, body = { + val plugin = createClientPlugin( + name, + body = { onRequest { request, content -> onRequest(request, content) } @@ -87,4 +100,4 @@ fun HttpClientConfig<*>.installClientPlugin(name: String, } ) install(plugin) -} \ No newline at end of file +} diff --git a/oidc-core/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/helper/FlowWrapper.kt b/oidc-core/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/helper/FlowWrapper.kt index 37b66623..720112e7 100644 --- a/oidc-core/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/helper/FlowWrapper.kt +++ b/oidc-core/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/helper/FlowWrapper.kt @@ -9,16 +9,17 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch // see https://proandroiddev.com/writing-swift-friendly-kotlin-multiplatform-apis-part-ix-flow-d4b6ada59395 -class FlowWrapper internal constructor(private val scope: CoroutineScope, - private val flow: Flow){ +public class FlowWrapper internal constructor( + private val scope: CoroutineScope, + private val flow: Flow +) { private var job: Job? = null private var isCancelled = false /** * Cancels the flow */ - @Suppress("unused") - fun cancel() { + public fun cancel() { isCancelled = true job?.cancel() } @@ -29,16 +30,15 @@ class FlowWrapper internal constructor(private val scope: CoroutineScope, * @param onCompletion callback called when flow completes. It will be provided with a non * nullable Throwable if it completes abnormally */ - @Suppress("unused") - fun collect( + public fun collect( onEach: (T) -> Unit, onCompletion: (Throwable?) -> Unit ) { if (isCancelled) return job = scope.launch { - flow.onEach (onEach).onCompletion { cause: Throwable? -> onCompletion(cause) }.collect {} + flow.onEach(onEach).onCompletion { cause: Throwable? -> onCompletion(cause) }.collect {} } } } -fun Flow.wrap(scope: CoroutineScope = MainScope()) = FlowWrapper(scope, this) \ No newline at end of file +public fun Flow.wrap(scope: CoroutineScope = MainScope()): FlowWrapper = FlowWrapper(scope, this) diff --git a/oidc-crypto/build.gradle.kts b/oidc-crypto/build.gradle.kts index 2629ec67..1991d21e 100644 --- a/oidc-crypto/build.gradle.kts +++ b/oidc-crypto/build.gradle.kts @@ -1,4 +1,3 @@ -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform import org.publicvalue.convention.config.configureIosTargets import org.publicvalue.convention.config.configureWasmTarget import org.publicvalue.convention.config.exportKdoc @@ -17,31 +16,25 @@ kotlin { configureIosTargets() configureWasmTarget() sourceSets { - val commonMain by getting { - dependencies { - implementation(libs.kotlinx.coroutines.core) + commonMain.dependencies { + implementation(libs.kotlinx.coroutines.core) - implementation(libs.ktor.utils) // for base64 encoding, oidc-core uses this anyways - } + implementation(libs.ktor.utils) // for base64 encoding, oidc-core uses this anyways } - val commonTest by getting { - dependencies { - implementation(projects.oidcCore) - implementation(kotlin("test")) - implementation(libs.assertk) - implementation(libs.kotlinx.coroutines.test) - } + commonTest.dependencies { + implementation(projects.oidcCore) + implementation(kotlin("test")) + implementation(libs.assertk) + implementation(libs.kotlinx.coroutines.test) } - val wasmJsMain by getting { - dependencies { - implementation(project.dependencies.platform(libs.kotlincrypto.hash.bom)) - implementation(libs.kotlincrypto.hash.sha2) + wasmJsMain.dependencies { + implementation(project.dependencies.platform(libs.kotlincrypto.hash.bom)) + implementation(libs.kotlincrypto.hash.sha2) - implementation(libs.ktor.utils) - implementation(libs.kotlinx.browser) - } + implementation(libs.ktor.utils) + implementation(libs.kotlinx.browser) } } diff --git a/oidc-crypto/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.kt b/oidc-crypto/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.kt index 505f77ad..c09d6b94 100644 --- a/oidc-crypto/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.kt +++ b/oidc-crypto/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.kt @@ -1,7 +1,6 @@ package org.publicvalue.multiplatform.oidc import io.ktor.util.encodeBase64 -import kotlin.random.Random /** * Generate random bytes using a cryptographically secure random @@ -15,13 +14,13 @@ import kotlin.random.Random * * @param size number of bytes to generate */ -expect fun secureRandomBytes(size: Int = 32): ByteArray +public expect fun secureRandomBytes(size: Int = 32): ByteArray /** * Implementation of base64urlencode, * see [RFC7636](https://datatracker.ietf.org/doc/html/rfc7636#appendix-A) */ -fun ByteArray.encodeForPKCE() = this.encodeBase64() +public fun ByteArray.encodeForPKCE(): String = this.encodeBase64() .replace("=", "") .replace("+", "-") .replace("/", "_") @@ -32,4 +31,4 @@ fun ByteArray.encodeForPKCE() = this.encodeBase64() * @receiver the string to hash * @return SHA-256 hash as byte array */ -expect fun String.s256(): ByteArray \ No newline at end of file +public expect fun String.s256(): ByteArray diff --git a/oidc-crypto/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/PKCETest.kt b/oidc-crypto/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/PKCETest.kt index 90c2fbbb..81e622ff 100644 --- a/oidc-crypto/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/PKCETest.kt +++ b/oidc-crypto/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/PKCETest.kt @@ -21,18 +21,32 @@ class PKCETest { fun testExamples() { val bytes = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".s256() assertThat(bytes).isEqualTo( - ubyteArrayOf(19u, 211u, 30u, 150u, 26u, 26u, 216u, 236u, 47u, 22u, 177u, 12u, 76u, 152u, 46u, - 8u, 118u, 168u, 120u, 173u, 109u, 241u, 68u, 86u, 110u, 225u, 137u, 74u, 203u, - 112u, 249u, 195u).toByteArray() + ubyteArrayOf( + 19u, 211u, 30u, 150u, 26u, 26u, 216u, 236u, 47u, 22u, 177u, 12u, 76u, 152u, 46u, + 8u, 118u, 168u, 120u, 173u, 109u, 241u, 68u, 86u, 110u, 225u, 137u, 74u, 203u, + 112u, 249u, 195u + ).toByteArray() ) - assertThat("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".s256().encodeForPKCE()).isEqualTo("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") + assertThat( + "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".s256().encodeForPKCE() + ).isEqualTo("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") - assertThat("OtVkSmgv4FG0Yunzad_Y41FfqBMeNv5R5t9Y1F0SCa4".s256().encodeForPKCE()).isEqualTo("6O-tieQY_-8QPlx2gHJf-kY8ZZsndw8WrYYFheRk4Uk") - assertThat("49f28fec29cfc6243be4348e20dbb3c45a5ca230dbd2508b61d3bb80".s256().encodeForPKCE()).isEqualTo("71_p-2XnQNtp0ejBitMKotd0KXC4mOrBGQCrV2NB_SM") - assertThat("gdFYQb_89x1lV7c0oHoCkJEQjRXhQzGRD4kmzYk1s5M".s256().encodeForPKCE()).isEqualTo("t0hcawUItzlHvJczV9dDP18E--htlBE1MlBd37OgZRc") - assertThat("FvquH4N_qH-YZRxkkfauVCXv0nWmLtbLjnAqQS5LVVU".s256().encodeForPKCE()).isEqualTo("QkfMjldvTK49kFrHLjwm1ZIy1SHkmKong4BmKBATIL0") - assertThat("12W5Fscr8w34Q6510F8MmiDoN3cBhA_XSWSONObhKUg".s256().encodeForPKCE()).isEqualTo("vPGsmc1pTnazk-Q9lwMOBrzG2wNdEa8KkYoVXq7Tknk") + assertThat( + "OtVkSmgv4FG0Yunzad_Y41FfqBMeNv5R5t9Y1F0SCa4".s256().encodeForPKCE() + ).isEqualTo("6O-tieQY_-8QPlx2gHJf-kY8ZZsndw8WrYYFheRk4Uk") + assertThat( + "49f28fec29cfc6243be4348e20dbb3c45a5ca230dbd2508b61d3bb80".s256().encodeForPKCE() + ).isEqualTo("71_p-2XnQNtp0ejBitMKotd0KXC4mOrBGQCrV2NB_SM") + assertThat( + "gdFYQb_89x1lV7c0oHoCkJEQjRXhQzGRD4kmzYk1s5M".s256().encodeForPKCE() + ).isEqualTo("t0hcawUItzlHvJczV9dDP18E--htlBE1MlBd37OgZRc") + assertThat( + "FvquH4N_qH-YZRxkkfauVCXv0nWmLtbLjnAqQS5LVVU".s256().encodeForPKCE() + ).isEqualTo("QkfMjldvTK49kFrHLjwm1ZIy1SHkmKong4BmKBATIL0") + assertThat( + "12W5Fscr8w34Q6510F8MmiDoN3cBhA_XSWSONObhKUg".s256().encodeForPKCE() + ).isEqualTo("vPGsmc1pTnazk-Q9lwMOBrzG2wNdEa8KkYoVXq7Tknk") } @Test @@ -55,9 +69,9 @@ class PKCETest { val codeVerifier = pkse.codeVerifier val codeChallenge = codeVerifier.s256().encodeForPKCE() - assertNotNull(codeChallenge, "Code challenge should not be null", ) + assertNotNull(codeChallenge, "Code challenge should not be null",) assertTrue("Code challenge must match Base64 URL format") { codeChallenge.matches(Regex("^[A-Za-z0-9-_]+$")) } } -} \ No newline at end of file +} diff --git a/oidc-crypto/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.ios.kt b/oidc-crypto/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.ios.kt index 10705438..153e9a98 100644 --- a/oidc-crypto/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.ios.kt +++ b/oidc-crypto/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.ios.kt @@ -10,7 +10,7 @@ import platform.Security.errSecSuccess import platform.Security.kSecRandomDefault @OptIn(ExperimentalForeignApi::class) -actual fun secureRandomBytes(size: Int): ByteArray { +public actual fun secureRandomBytes(size: Int): ByteArray { val bytes = ByteArray(size) bytes.usePinned { pin -> @@ -25,14 +25,18 @@ actual fun secureRandomBytes(size: Int): ByteArray { } @OptIn(ExperimentalForeignApi::class) -actual fun String.s256(): ByteArray { +public actual fun String.s256(): ByteArray { val inputBytes = this.encodeToByteArray() - val output = UByteArray(32)// 32 bytes for the SHA-256 + + @Suppress("MagicNumber") + val output = UByteArray(32) // 32 bytes for the SHA-256 inputBytes.usePinned { inputPin -> output.usePinned { outputPin -> CC_SHA256( - inputPin.addressOf(0), inputBytes.size.toUInt(), outputPin.addressOf(0) + inputPin.addressOf(0), + inputBytes.size.toUInt(), + outputPin.addressOf(0) ) } } diff --git a/oidc-crypto/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.jvm.kt b/oidc-crypto/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.jvm.kt index f413ce9f..0dba6cc5 100644 --- a/oidc-crypto/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.jvm.kt +++ b/oidc-crypto/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.jvm.kt @@ -3,13 +3,13 @@ package org.publicvalue.multiplatform.oidc import java.security.MessageDigest import java.security.SecureRandom -actual fun secureRandomBytes(size: Int): ByteArray { +public actual fun secureRandomBytes(size: Int): ByteArray { val bytes = ByteArray(size) SecureRandom().nextBytes(bytes) return bytes } -actual fun String.s256(): ByteArray { +public actual fun String.s256(): ByteArray { val sha256 = MessageDigest.getInstance("SHA256") return sha256.digest(this.toByteArray(charset = Charsets.US_ASCII)) -} \ No newline at end of file +} diff --git a/oidc-crypto/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.wasmJs.kt b/oidc-crypto/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.wasmJs.kt index 9623a363..010bb8ca 100644 --- a/oidc-crypto/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.wasmJs.kt +++ b/oidc-crypto/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/Crypto.wasmJs.kt @@ -1,27 +1,28 @@ package org.publicvalue.multiplatform.oidc -import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.toByteArray import org.khronos.webgl.Uint8Array import org.khronos.webgl.get import org.kotlincrypto.hash.sha2.SHA256 - /** * this delegates to [`crypto.getRandomValues`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) */ -external object crypto { +@JsName("crypto") +private external object Crypto { + @Suppress("unused") fun getRandomValues(uint8Array: Uint8Array) } -actual fun secureRandomBytes(size: Int): ByteArray { +public actual fun secureRandomBytes(size: Int): ByteArray { require(size >= 0) { "count cannot be negative" } val uint8Array = Uint8Array(size) - crypto.getRandomValues(uint8Array) - return ByteArray(size) { uint8Array[it].toByte() } + Crypto.getRandomValues(uint8Array = uint8Array) + return ByteArray(size) { uint8Array[it] } } -actual fun String.s256(): ByteArray { +public actual fun String.s256(): ByteArray { val digest = SHA256() digest.update(this.toByteArray()) return digest.digest() -} \ No newline at end of file +} diff --git a/oidc-ktor/build.gradle.kts b/oidc-ktor/build.gradle.kts index 97b3dfd6..7371e61e 100644 --- a/oidc-ktor/build.gradle.kts +++ b/oidc-ktor/build.gradle.kts @@ -15,21 +15,18 @@ kotlin { configureIosTargets() configureWasmTarget() sourceSets { - val commonMain by getting { - dependencies { - api(projects.oidcCore) - implementation(projects.oidcTokenstore) - implementation(libs.ktor.client.auth) - } + commonMain.dependencies { + api(projects.oidcCore) + implementation(projects.oidcTokenstore) + implementation(libs.ktor.client.auth) } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - } + + commonTest.dependencies { + implementation(kotlin("test")) } } } android { namespace = "org.publicvalue.multiplatform.oidc.appsupport.ktor" -} \ No newline at end of file +} diff --git a/oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/ConfigureAuthPlugin.kt b/oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/ConfigureAuthPlugin.kt index 6eb620ed..59880125 100644 --- a/oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/ConfigureAuthPlugin.kt +++ b/oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/ConfigureAuthPlugin.kt @@ -16,7 +16,7 @@ import org.publicvalue.multiplatform.oidc.tokenstore.removeTokens * Configure Bearer Authentication using TokenStore + RefreshHandler. */ @ExperimentalOpenIdConnect -fun AuthConfig.oidcBearer( +public fun AuthConfig.oidcBearer( tokenStore: TokenStore, refreshHandler: TokenRefreshHandler, client: OpenIdConnectClient, @@ -40,7 +40,7 @@ fun AuthConfig.oidcBearer( * save it into the store. */ @ExperimentalOpenIdConnect -fun AuthConfig.oidcBearer( +public fun AuthConfig.oidcBearer( tokenStore: TokenStore, /** receives the old access token as parameter. * This function should get new tokens and save them. @@ -49,7 +49,6 @@ fun AuthConfig.oidcBearer( /** called when refresh throws **/ onRefreshFailed: suspend (Exception) -> Unit ) { - bearer { loadTokens( tokenStore = tokenStore @@ -66,7 +65,7 @@ fun AuthConfig.oidcBearer( * Load tokens from given token store. */ @ExperimentalOpenIdConnect -fun BearerAuthConfig.loadTokens(tokenStore: TokenStore) { +public fun BearerAuthConfig.loadTokens(tokenStore: TokenStore) { loadTokens { val accessToken = tokenStore.getAccessToken() val refreshToken = tokenStore.getRefreshToken() @@ -88,7 +87,7 @@ fun BearerAuthConfig.loadTokens(tokenStore: TokenStore) { * @param onRefreshFailed called when the refresh throws an exception */ @ExperimentalOpenIdConnect -fun BearerAuthConfig.refreshTokens( +public fun BearerAuthConfig.refreshTokens( /** receives the old access token **/ refreshAndSaveTokens: suspend (String) -> OauthTokens?, /** called when refresh throws **/ @@ -97,10 +96,10 @@ fun BearerAuthConfig.refreshTokens( refreshTokens { val newTokens = try { refreshAndSaveTokens(this.oldTokens?.accessToken.orEmpty()) - } catch (e: OpenIdConnectException) { - if (e is OpenIdConnectException.UnsuccessfulTokenRequest) { - onRefreshFailed(e) - } + } catch (e: OpenIdConnectException.UnsuccessfulTokenRequest) { + onRefreshFailed(e) + null + } catch (_: OpenIdConnectException) { null } newTokens?.let { diff --git a/oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/HttpClient+clearTokens.kt b/oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/HttpClientExtensions.kt similarity index 78% rename from oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/HttpClient+clearTokens.kt rename to oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/HttpClientExtensions.kt index 66e3d4bc..f329072d 100644 --- a/oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/HttpClient+clearTokens.kt +++ b/oidc-ktor/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/ktor/HttpClientExtensions.kt @@ -1,10 +1,8 @@ package org.publicvalue.multiplatform.oidc.ktor import io.ktor.client.HttpClient -import io.ktor.client.plugins.auth.Auth import io.ktor.client.plugins.auth.authProvider import io.ktor.client.plugins.auth.providers.BearerAuthProvider -import io.ktor.client.plugins.plugin /** * Force the Auth plugin to invoke the `loadTokens` block again on the next client request. @@ -12,7 +10,6 @@ import io.ktor.client.plugins.plugin * @see https://youtrack.jetbrains.com/issue/KTOR-4759/Auth-BearerAuthProvider-caches-result-of-loadToken-until-process-death */ -@Suppress("unused") -fun HttpClient.clearTokens() { +public fun HttpClient.clearTokens() { authProvider()?.clearToken() -} \ No newline at end of file +} diff --git a/oidc-ktor/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/ktor/README.kt b/oidc-ktor/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/ktor/README.kt index d61dd738..27d0da96 100644 --- a/oidc-ktor/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/ktor/README.kt +++ b/oidc-ktor/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/ktor/README.kt @@ -27,5 +27,4 @@ object README { } } } - -} \ No newline at end of file +} diff --git a/oidc-okhttp4/build.gradle.kts b/oidc-okhttp4/build.gradle.kts index 5f02dba0..7434eb75 100644 --- a/oidc-okhttp4/build.gradle.kts +++ b/oidc-okhttp4/build.gradle.kts @@ -8,26 +8,17 @@ description = "Kotlin Multiplatform OIDC support library for Android OkHttp" kotlin { sourceSets { - val commonMain by getting { - dependencies { - api(projects.oidcCore) - implementation(projects.oidcTokenstore) - } + commonMain.dependencies { + api(projects.oidcCore) + implementation(projects.oidcTokenstore) } - val androidMain by getting { - dependencies { - implementation(libs.okhttp) - } - } - - val commonTest by getting { - dependencies { - } + androidMain.dependencies { + implementation(libs.okhttp) } } } android { namespace = "org.publicvalue.multiplatform.oidc.appsupport.okhttp" -} \ No newline at end of file +} diff --git a/oidc-okhttp4/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/okhttp/DefaultOpenIdConnectAuthenticator.kt b/oidc-okhttp4/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/okhttp/DefaultOpenIdConnectAuthenticator.kt index da73cf16..d0eebed4 100644 --- a/oidc-okhttp4/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/okhttp/DefaultOpenIdConnectAuthenticator.kt +++ b/oidc-okhttp4/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/okhttp/DefaultOpenIdConnectAuthenticator.kt @@ -15,11 +15,11 @@ import org.publicvalue.multiplatform.oidc.tokenstore.removeTokens */ @ExperimentalOpenIdConnect @Suppress("unused") -open class DefaultOpenIdConnectAuthenticator( - open val tokenStore: TokenStore, - open val refreshHandler: TokenRefreshHandler, - open val client: OpenIdConnectClient -): OpenIdConnectAuthenticator() { +public open class DefaultOpenIdConnectAuthenticator( + public open val tokenStore: TokenStore, + public open val refreshHandler: TokenRefreshHandler, + public open val client: OpenIdConnectClient +) : OpenIdConnectAuthenticator() { override suspend fun getAccessToken(): String? { return tokenStore.getAccessToken() } @@ -37,4 +37,4 @@ open class DefaultOpenIdConnectAuthenticator( override fun buildRequest(builder: Request.Builder) { super.buildRequest(builder) } -} \ No newline at end of file +} diff --git a/oidc-okhttp4/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/okhttp/OpenIdConnectAuthenticator.kt b/oidc-okhttp4/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/okhttp/OpenIdConnectAuthenticator.kt index ba4291ba..fe7c3935 100644 --- a/oidc-okhttp4/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/okhttp/OpenIdConnectAuthenticator.kt +++ b/oidc-okhttp4/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/okhttp/OpenIdConnectAuthenticator.kt @@ -10,7 +10,7 @@ import okhttp3.Route import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect import org.publicvalue.multiplatform.oidc.tokenstore.OauthTokens -private val LOG_TAG = "OIDCAuthenticator" +private const val LOG_TAG = "OIDCAuthenticator" /** * OkHttp Authenticator. @@ -20,19 +20,29 @@ private val LOG_TAG = "OIDCAuthenticator" * If a 401 is encountered with access token set, it will perform a refresh. */ @ExperimentalOpenIdConnect -abstract class OpenIdConnectAuthenticator: Authenticator { +public abstract class OpenIdConnectAuthenticator : Authenticator { override fun authenticate(route: Route?, response: Response): Request? { // authenticator steps in when 401 is received if (response.request.header(HttpHeaders.Authorization) == null) { - Log.d(LOG_TAG, "Get token for authenticated call. Got ${response.code} with no Authorization header set") + Log.d( + LOG_TAG, + "Get token for authenticated call. Got ${response.code} with no Authorization header set" + ) } else { - Log.d(LOG_TAG, "Get token for authenticated call. Got ${response.code} WITH Authorization header set") + Log.d( + LOG_TAG, + "Get token for authenticated call. Got ${response.code} WITH Authorization header set" + ) } - + @Suppress("MagicNumber") val accessToken = runBlocking { try { val token = getAccessToken() - if (token != null && response.code == 401 && response.request.header(HttpHeaders.Authorization)?.contains(token) == true) { + if (token != null && response.code == 401 && + response.request + .header(HttpHeaders.Authorization) + ?.contains(token) == true + ) { // Got 401 -> refresh token Log.d(LOG_TAG, "Refreshing access token as using it returned a 401") val newTokens = refreshTokens(oldAccessToken = token) @@ -46,7 +56,9 @@ abstract class OpenIdConnectAuthenticator: Authenticator { } } - return if (accessToken != null && response.request.header(HttpHeaders.Authorization)?.contains(accessToken) != true) { // != true is correct + return if (accessToken != null && response.request.header(HttpHeaders.Authorization) + ?.contains(accessToken) != true + ) { // != true is correct response.request.newBuilder() .header(HttpHeaders.Authorization, "Bearer $accessToken") .apply { buildRequest(this) } @@ -59,64 +71,67 @@ abstract class OpenIdConnectAuthenticator: Authenticator { } } - abstract suspend fun getAccessToken(): String? - abstract suspend fun refreshTokens(oldAccessToken: String): OauthTokens? - abstract fun onRefreshFailed() + public abstract suspend fun getAccessToken(): String? + public abstract suspend fun refreshTokens(oldAccessToken: String): OauthTokens? + public abstract fun onRefreshFailed() + /** Override to provide additional configuration for the authenticated request **/ - open fun buildRequest(builder: Request.Builder) {} + public open fun buildRequest(builder: Request.Builder) {} } @ExperimentalOpenIdConnect -@Suppress("unused") -data class OpenIdConnectAuthenticatorConfig( +@Suppress("ForbiddenPublicDataClass") +public data class OpenIdConnectAuthenticatorConfig( internal var getAccessToken: (suspend () -> String?)? = null, internal var refreshTokens: (suspend (oldAccessToken: String) -> OauthTokens?)? = null, internal var onRefreshFailed: (() -> Unit)? = null, internal var buildRequest: Request.Builder.() -> Unit = {} ) { - fun getAccessToken(block: suspend () -> String?) { + public fun getAccessToken(block: suspend () -> String?) { getAccessToken = block } - fun refreshTokens(block: suspend (oldAccessToken: String) -> OauthTokens) { + public fun refreshTokens(block: suspend (oldAccessToken: String) -> OauthTokens) { refreshTokens = block } - fun onRefreshFailed(block: () -> Unit) { + public fun onRefreshFailed(block: () -> Unit) { onRefreshFailed = block } - fun buildRequest(block: Request.Builder.() -> Unit) { + public fun buildRequest(block: Request.Builder.() -> Unit) { buildRequest = block } internal fun validate() { - if (getAccessToken == null) { - throw IllegalArgumentException("getAccessToken() must be configured") + requireNotNull(getAccessToken) { + "getAccessToken() must be configured" } } } @ExperimentalOpenIdConnect -@Suppress("unused") -fun OpenIdConnectAuthenticator( +public fun OpenIdConnectAuthenticator( configureBlock: OpenIdConnectAuthenticatorConfig.() -> Unit ): OpenIdConnectAuthenticator { val config = OpenIdConnectAuthenticatorConfig() .apply { configureBlock() } config.validate() - return object: OpenIdConnectAuthenticator() { + return object : OpenIdConnectAuthenticator() { override suspend fun getAccessToken(): String? { return config.getAccessToken?.invoke() } + override suspend fun refreshTokens(oldAccessToken: String): OauthTokens? { return config.refreshTokens?.invoke(oldAccessToken) } + override fun onRefreshFailed() { config.onRefreshFailed?.invoke() } + override fun buildRequest(builder: Request.Builder) { config.buildRequest(builder) } } -} \ No newline at end of file +} diff --git a/oidc-okhttp4/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/okhttp/README.kt b/oidc-okhttp4/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/okhttp/Readme.kt similarity index 95% rename from oidc-okhttp4/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/okhttp/README.kt rename to oidc-okhttp4/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/okhttp/Readme.kt index b5dd1f4c..fc3afdcc 100644 --- a/oidc-okhttp4/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/okhttp/README.kt +++ b/oidc-okhttp4/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/okhttp/Readme.kt @@ -8,13 +8,13 @@ import org.publicvalue.multiplatform.oidc.tokenstore.TokenStore // ok http readme section @OptIn(ExperimentalOpenIdConnect::class) +@Suppress("unused") object Readme { val tokenStore: TokenStore = null!! val refreshHandler: TokenRefreshHandler = null!! val client: OpenIdConnectClient = null!! - fun `configureOkHttpAuth`() { - + fun configureOkHttpAuth() { @OptIn(ExperimentalOpenIdConnect::class) val authenticator = OpenIdConnectAuthenticator { getAccessToken { tokenStore.getAccessToken() } diff --git a/oidc-tokenstore/build.gradle.kts b/oidc-tokenstore/build.gradle.kts index ce313fc5..7ddb0da5 100644 --- a/oidc-tokenstore/build.gradle.kts +++ b/oidc-tokenstore/build.gradle.kts @@ -15,31 +15,25 @@ kotlin { configureIosTargets() configureWasmTarget() sourceSets { - val commonMain by getting { - dependencies { - api(projects.oidcCore) - implementation(libs.russhwolf.multiplatformsettings) + commonMain.dependencies { + api(projects.oidcCore) + implementation(libs.russhwolf.multiplatformsettings) - api(libs.kotlinx.serialization.json) - } + api(libs.kotlinx.serialization.json) } - val androidMain by getting { - dependencies { - implementation(libs.androidx.security.crypto) - implementation(libs.androidx.security.crypto.ktx) + androidMain.dependencies { + implementation(libs.androidx.security.crypto) + implementation(libs.androidx.security.crypto.ktx) - implementation(libs.androidx.datastore) - } + implementation(libs.androidx.datastore) } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - implementation(libs.assertk) - implementation(libs.kotlinx.coroutines.test) - implementation(projects.oidcAppsupport) // for readme - } + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.assertk) + implementation(libs.kotlinx.coroutines.test) + implementation(projects.oidcAppsupport) // for readme } } -} \ No newline at end of file +} diff --git a/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidDataStoreSettingsStore.kt b/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidDataStoreSettingsStore.kt index 7d4d7a34..adfaae90 100644 --- a/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidDataStoreSettingsStore.kt +++ b/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidDataStoreSettingsStore.kt @@ -18,11 +18,11 @@ import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec @RequiresApi(Build.VERSION_CODES.M) -class AndroidDataStoreSettingsStore( +public class AndroidDataStoreSettingsStore( private val context: Context ) : SettingsStore { - companion object { + private companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" private const val KEY_ALIAS = "OidcDataStoreEncryptionKey" private const val TRANSFORMATION = "AES/GCM/NoPadding" @@ -36,6 +36,7 @@ class AndroidDataStoreSettingsStore( ) // Get or generate the secret key + @Suppress("MagicNumber") private val secretKey: SecretKey by lazy { val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } if (keyStore.containsAlias(KEY_ALIAS)) { diff --git a/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidEncryptedPreferencesSettingsStore.kt b/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidEncryptedPreferencesSettingsStore.kt index 8d92b856..ed7e3704 100644 --- a/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidEncryptedPreferencesSettingsStore.kt +++ b/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidEncryptedPreferencesSettingsStore.kt @@ -11,7 +11,7 @@ import com.russhwolf.settings.get "EncryptedSharedPreferences is deprecated", replaceWith = ReplaceWith("AndroidDataStoreSettingsStore") ) -class AndroidEncryptedPreferencesSettingsStore( +public class AndroidEncryptedPreferencesSettingsStore( context: Context ) : SettingsStore { @@ -45,5 +45,4 @@ class AndroidEncryptedPreferencesSettingsStore( override suspend fun clear() { settings.clear() } - -} \ No newline at end of file +} diff --git a/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidSettingsTokenStore.kt b/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidSettingsTokenStore.kt index d8befa5b..6d1638f1 100644 --- a/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidSettingsTokenStore.kt +++ b/oidc-tokenstore/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/AndroidSettingsTokenStore.kt @@ -5,14 +5,13 @@ import android.os.Build import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect @ExperimentalOpenIdConnect -@Suppress("unused") -class AndroidSettingsTokenStore( +public class AndroidSettingsTokenStore( context: Context -): SettingsTokenStore( +) : SettingsTokenStore( settings = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { AndroidDataStoreSettingsStore(context) } else { @Suppress("DEPRECATION") AndroidEncryptedPreferencesSettingsStore(context) } -) \ No newline at end of file +) diff --git a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/OauthTokens.kt b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/OauthTokens.kt index 11b8ed1a..48aea718 100644 --- a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/OauthTokens.kt +++ b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/OauthTokens.kt @@ -1,7 +1,7 @@ package org.publicvalue.multiplatform.oidc.tokenstore - -data class OauthTokens( +@Suppress("ForbiddenPublicDataClass") +public data class OauthTokens( val accessToken: String, val refreshToken: String?, val idToken: String? -) \ No newline at end of file +) diff --git a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsStore.kt b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsStore.kt index 6602c198..b2cdd3de 100644 --- a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsStore.kt +++ b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsStore.kt @@ -5,9 +5,9 @@ import kotlin.native.HiddenFromObjC @OptIn(ExperimentalObjCRefinement::class) @HiddenFromObjC -interface SettingsStore { - suspend fun get(key: String): String? - suspend fun put(key: String, value: String) - suspend fun remove(key: String) - suspend fun clear() -} \ No newline at end of file +public interface SettingsStore { + public suspend fun get(key: String): String? + public suspend fun put(key: String, value: String) + public suspend fun remove(key: String) + public suspend fun clear() +} diff --git a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStore.kt b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStore.kt index a4623e6a..b022a4f2 100644 --- a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStore.kt +++ b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStore.kt @@ -1,5 +1,6 @@ package org.publicvalue.multiplatform.oidc.tokenstore +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow @@ -7,7 +8,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect -enum class SettingsKey { +public enum class SettingsKey { ACCESSTOKEN, REFRESHTOKEN, IDTOKEN } @@ -16,9 +17,9 @@ enum class SettingsKey { * iOS implementation: [KeychainTokenStore] */ @ExperimentalOpenIdConnect -open class SettingsTokenStore( +public open class SettingsTokenStore( private val settings: SettingsStore -): TokenStore() { +) : TokenStore() { private val mutex = Mutex(false) @@ -30,29 +31,32 @@ open class SettingsTokenStore( private var refreshTokenLoaded = false private var idTokenLoaded = false - override val accessTokenFlow get() = flow { - if (!accessTokenLoaded) { - accessTokenLoaded = true - currentAccessToken.value = getAccessToken() + override val accessTokenFlow: Flow + get() = flow { + if (!accessTokenLoaded) { + accessTokenLoaded = true + currentAccessToken.value = getAccessToken() + } + emitAll(currentAccessToken) } - emitAll(currentAccessToken) - } - override val refreshTokenFlow get() = flow { - if (!refreshTokenLoaded) { - refreshTokenLoaded = true - currentRefreshToken.value = getRefreshToken() + override val refreshTokenFlow: Flow + get() = flow { + if (!refreshTokenLoaded) { + refreshTokenLoaded = true + currentRefreshToken.value = getRefreshToken() + } + emitAll(currentRefreshToken) } - emitAll(currentRefreshToken) - } - override val idTokenFlow get() = flow { - if (!idTokenLoaded) { - idTokenLoaded = true - currentIdToken.value = getIdToken() + override val idTokenFlow: Flow + get() = flow { + if (!idTokenLoaded) { + idTokenLoaded = true + currentIdToken.value = getIdToken() + } + emitAll(currentIdToken) } - emitAll(currentIdToken) - } override suspend fun getAccessToken(): String? { return runOrNull { @@ -129,9 +133,9 @@ open class SettingsTokenStore( } // catch anything to avoid crashes on ios -inline fun runOrNull(block: () -> T?): T? = try { +private inline fun runOrNull(block: () -> T?): T? = try { block() } catch (t: Throwable) { println(t.message) null -} \ No newline at end of file +} diff --git a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler.kt b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler.kt index be378a16..242e6d3f 100644 --- a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler.kt +++ b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler.kt @@ -18,8 +18,7 @@ import kotlin.native.ObjCName @ExperimentalOpenIdConnect @OptIn(ExperimentalObjCName::class, ExperimentalObjCRefinement::class) @ObjCName("TokenRefreshHandler", "TokenRefreshHandler", exact = true) -@Suppress("unused") -class TokenRefreshHandler( +public class TokenRefreshHandler( private val tokenStore: TokenStore, ) { private val mutex = Mutex() @@ -29,7 +28,7 @@ class TokenRefreshHandler( * @return The new tokens */ @Throws(OpenIdConnectException::class, CancellationException::class) - suspend fun refreshAndSaveToken(client: OpenIdConnectClient, oldAccessToken: String): OauthTokens { + public suspend fun refreshAndSaveToken(client: OpenIdConnectClient, oldAccessToken: String): OauthTokens { return refreshAndSaveToken(client::refreshToken, oldAccessToken) } @@ -43,7 +42,10 @@ class TokenRefreshHandler( */ @Throws(OpenIdConnectException::class, CancellationException::class) @HiddenFromObjC - suspend fun refreshAndSaveToken(refreshCall: suspend (String) -> AccessTokenResponse, oldAccessToken: String): OauthTokens { + public suspend fun refreshAndSaveToken( + refreshCall: suspend (String) -> AccessTokenResponse, + oldAccessToken: String + ): OauthTokens { mutex.withLock { val currentTokens = tokenStore.getTokens() return if (currentTokens != null && currentTokens.accessToken != oldAccessToken) { @@ -51,15 +53,15 @@ class TokenRefreshHandler( } else { val refreshToken = tokenStore.getRefreshToken() var newTokens = refreshCall(refreshToken ?: "") - if(newTokens.refresh_token == null) { - newTokens = newTokens.copy(refresh_token = refreshToken) + if (newTokens.refreshToken == null) { + newTokens = newTokens.copy(refreshToken = refreshToken) } tokenStore.saveTokens(newTokens) OauthTokens( - accessToken = newTokens.access_token, - refreshToken = newTokens.refresh_token, - idToken = newTokens.id_token + accessToken = newTokens.accessToken, + refreshToken = newTokens.refreshToken, + idToken = newTokens.idToken ) } } diff --git a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenStore.kt b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenStore.kt index 9825775c..6e78ad42 100644 --- a/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenStore.kt +++ b/oidc-tokenstore/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenStore.kt @@ -17,35 +17,35 @@ import kotlin.native.ObjCName @OptIn(ExperimentalObjCName::class) @ObjCName("TokenStoreProtocol", "TokenStoreProtocol", exact = true) // not an interface to support extension methods in swift -abstract class TokenStore { - abstract suspend fun getAccessToken(): String? - abstract suspend fun getRefreshToken(): String? - abstract suspend fun getIdToken(): String? +public abstract class TokenStore { + public abstract suspend fun getAccessToken(): String? + public abstract suspend fun getRefreshToken(): String? + public abstract suspend fun getIdToken(): String? - abstract val accessTokenFlow: Flow - abstract val refreshTokenFlow: Flow - abstract val idTokenFlow: Flow + public abstract val accessTokenFlow: Flow + public abstract val refreshTokenFlow: Flow + public abstract val idTokenFlow: Flow - abstract suspend fun removeAccessToken() - abstract suspend fun removeRefreshToken() - abstract suspend fun removeIdToken() + public abstract suspend fun removeAccessToken() + public abstract suspend fun removeRefreshToken() + public abstract suspend fun removeIdToken() - abstract suspend fun saveTokens(accessToken: String, refreshToken: String?, idToken: String?) + public abstract suspend fun saveTokens(accessToken: String, refreshToken: String?, idToken: String?) } // extension method so no need to overwrite in swift subclasses @ExperimentalOpenIdConnect -suspend fun TokenStore.saveTokens(tokens: AccessTokenResponse) { +public suspend fun TokenStore.saveTokens(tokens: AccessTokenResponse) { saveTokens( - accessToken = tokens.access_token, - refreshToken = tokens.refresh_token, - idToken = tokens.id_token + accessToken = tokens.accessToken, + refreshToken = tokens.refreshToken, + idToken = tokens.idToken ) } // extension method so no need to overwrite in swift subclasses @ExperimentalOpenIdConnect -suspend fun TokenStore.removeTokens() { +public suspend fun TokenStore.removeTokens() { removeAccessToken() removeIdToken() removeRefreshToken() @@ -53,7 +53,7 @@ suspend fun TokenStore.removeTokens() { // extension method so no need to overwrite in swift subclasses @ExperimentalOpenIdConnect -suspend fun TokenStore.getTokens(): OauthTokens? { +public suspend fun TokenStore.getTokens(): OauthTokens? { val accessToken = getAccessToken() val refreshToken = getRefreshToken() val idToken = getIdToken() @@ -70,7 +70,7 @@ suspend fun TokenStore.getTokens(): OauthTokens? { } @ExperimentalOpenIdConnect -val TokenStore.tokensFlow: Flow +public val TokenStore.tokensFlow: Flow get() = combine(accessTokenFlow, refreshTokenFlow, idTokenFlow) { accessToken, refreshToken, idToken -> if (accessToken != null) { OauthTokens( @@ -81,4 +81,4 @@ val TokenStore.tokensFlow: Flow } else { null } - } \ No newline at end of file + } diff --git a/oidc-tokenstore/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/InMemorySettingsStore.kt b/oidc-tokenstore/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/InMemorySettingsStore.kt index de5a7184..7a1f9ab2 100644 --- a/oidc-tokenstore/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/InMemorySettingsStore.kt +++ b/oidc-tokenstore/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/InMemorySettingsStore.kt @@ -5,7 +5,7 @@ import kotlin.native.HiddenFromObjC @OptIn(ExperimentalObjCRefinement::class) @HiddenFromObjC -class InMemorySettingsStore: SettingsStore { +class InMemorySettingsStore : SettingsStore { private val memory = hashMapOf() @@ -22,4 +22,4 @@ class InMemorySettingsStore: SettingsStore { override suspend fun clear() { memory.clear() } -} \ No newline at end of file +} diff --git a/oidc-tokenstore/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenStoreTest.kt b/oidc-tokenstore/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenStoreTest.kt index 832c9357..b8f36fa1 100644 --- a/oidc-tokenstore/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenStoreTest.kt +++ b/oidc-tokenstore/src/commonTest/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenStoreTest.kt @@ -10,7 +10,7 @@ import kotlin.test.Test @OptIn(ExperimentalOpenIdConnect::class) class TokenStoreTest { - private val tokenStore:TokenStore = SettingsTokenStore(settings = InMemorySettingsStore()) + private val tokenStore: TokenStore = SettingsTokenStore(settings = InMemorySettingsStore()) @Test fun saveRestore() = runTest { @@ -30,5 +30,4 @@ class TokenStoreTest { assertThat(tokenStore.getRefreshToken()).isEqualTo("2") assertThat(tokenStore.getIdToken()).isEqualTo("3") } - -} \ No newline at end of file +} diff --git a/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/IosKeychainSettingsStore.kt b/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/IosKeychainSettingsStore.kt index 995db29b..d11625a7 100644 --- a/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/IosKeychainSettingsStore.kt +++ b/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/IosKeychainSettingsStore.kt @@ -14,7 +14,7 @@ import kotlin.experimental.ExperimentalObjCRefinement @OptIn(ExperimentalObjCRefinement::class, ExperimentalSettingsImplementation::class) @HiddenFromObjC -class IosKeychainSettingsStore: SettingsStore { +public class IosKeychainSettingsStore : SettingsStore { @OptIn(ExperimentalForeignApi::class, ExperimentalSettingsApi::class) private val keyChainSettings by lazy { @@ -39,6 +39,4 @@ class IosKeychainSettingsStore: SettingsStore { override suspend fun clear() { keyChainSettings.clear() } - - -} \ No newline at end of file +} diff --git a/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/IosKeychainTokenStore.kt b/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/IosKeychainTokenStore.kt index 40a41c88..31cf5583 100644 --- a/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/IosKeychainTokenStore.kt +++ b/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/IosKeychainTokenStore.kt @@ -6,8 +6,7 @@ import kotlin.experimental.ExperimentalObjCName @ExperimentalOpenIdConnect @OptIn(ExperimentalObjCName::class) @ObjCName("KeychainTokenStore", "KeychainTokenStore", exact = true) -@Suppress("unused") /** * Uses the keychain to save and retrieve tokens. */ -class IosKeychainTokenStore: SettingsTokenStore(settings = IosKeychainSettingsStore()) \ No newline at end of file +public class IosKeychainTokenStore : SettingsTokenStore(settings = IosKeychainSettingsStore()) diff --git a/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStore+flowWrap.kt b/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStoreExtensionsFlowWrap.kt similarity index 63% rename from oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStore+flowWrap.kt rename to oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStoreExtensionsFlowWrap.kt index 39593e78..981f5aab 100644 --- a/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStore+flowWrap.kt +++ b/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/SettingsTokenStoreExtensionsFlowWrap.kt @@ -7,15 +7,12 @@ import kotlin.experimental.ExperimentalObjCName @OptIn(ExperimentalOpenIdConnect::class, ExperimentalObjCName::class) @ObjCName("accessToken") -@Suppress("unused") -val TokenStore.accessTokenFlowWrap: FlowWrapper get() = this.accessTokenFlow.wrap() +public val TokenStore.accessTokenFlowWrap: FlowWrapper get() = this.accessTokenFlow.wrap() @OptIn(ExperimentalOpenIdConnect::class, ExperimentalObjCName::class) @ObjCName("refreshToken") -@Suppress("unused") -val TokenStore.refreshTokenFlowWrap: FlowWrapper get() = this.refreshTokenFlow.wrap() +public val TokenStore.refreshTokenFlowWrap: FlowWrapper get() = this.refreshTokenFlow.wrap() @OptIn(ExperimentalOpenIdConnect::class, ExperimentalObjCName::class) @ObjCName("idToken") -@Suppress("unused") -val TokenStore.idTokenFlowWrap: FlowWrapper get() = this.idTokenFlow.wrap() \ No newline at end of file +public val TokenStore.idTokenFlowWrap: FlowWrapper get() = this.idTokenFlow.wrap() diff --git a/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler+iOS.kt b/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler.ios.kt similarity index 77% rename from oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler+iOS.kt rename to oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler.ios.kt index 6886ce08..843d76ef 100644 --- a/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler+iOS.kt +++ b/oidc-tokenstore/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/tokenstore/TokenRefreshHandler.ios.kt @@ -13,8 +13,10 @@ import kotlin.coroutines.cancellation.CancellationException */ @OptIn(ExperimentalOpenIdConnect::class) @Throws(OpenIdConnectException::class, CancellationException::class) -@Suppress("unused") -suspend fun TokenRefreshHandler.refreshAndSaveToken(refresher: TokenRefresher, oldAccessToken: String): OauthTokens { +public suspend fun TokenRefreshHandler.refreshAndSaveToken( + refresher: TokenRefresher, + oldAccessToken: String +): OauthTokens { return refreshAndSaveToken( refreshCall = { refreshToken -> refresher.refreshToken(refreshToken = refreshToken) @@ -26,6 +28,6 @@ suspend fun TokenRefreshHandler.refreshAndSaveToken(refresher: TokenRefresher, o /** * For iOS because we cannot accept lambdas as parameters yet */ -interface TokenRefresher { - suspend fun refreshToken(refreshToken: String): AccessTokenResponse -} \ No newline at end of file +public interface TokenRefresher { + public suspend fun refreshToken(refreshToken: String): AccessTokenResponse +} diff --git a/playground-app/common/screens/build.gradle.kts b/playground-app/common/screens/build.gradle.kts index 559e111d..8ddabd48 100644 --- a/playground-app/common/screens/build.gradle.kts +++ b/playground-app/common/screens/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode import org.publicvalue.convention.addParcelizeAnnotation plugins { @@ -6,6 +7,7 @@ plugins { } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/common/ui/compose/build.gradle.kts b/playground-app/common/ui/compose/build.gradle.kts index 17fc7b9b..b2896574 100644 --- a/playground-app/common/ui/compose/build.gradle.kts +++ b/playground-app/common/ui/compose/build.gradle.kts @@ -1,9 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") id("org.publicvalue.convention.compose.multiplatform") } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/common/ui/resources/strings/build.gradle.kts b/playground-app/common/ui/resources/strings/build.gradle.kts index 2484ea22..a5cb238c 100644 --- a/playground-app/common/ui/resources/strings/build.gradle.kts +++ b/playground-app/common/ui/resources/strings/build.gradle.kts @@ -1,8 +1,11 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/core/build.gradle.kts b/playground-app/core/build.gradle.kts index 99b8de0b..7cc00b38 100644 --- a/playground-app/core/build.gradle.kts +++ b/playground-app/core/build.gradle.kts @@ -1,9 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") alias(libs.plugins.ksp) } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/data/db-sqldelight/build.gradle.kts b/playground-app/data/db-sqldelight/build.gradle.kts index b577a9b3..00d328fc 100644 --- a/playground-app/data/db-sqldelight/build.gradle.kts +++ b/playground-app/data/db-sqldelight/build.gradle.kts @@ -1,9 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") alias(libs.plugins.sqldelight) } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/desktop-app/build.gradle.kts b/playground-app/desktop-app/build.gradle.kts index 95332b07..216d67d1 100644 --- a/playground-app/desktop-app/build.gradle.kts +++ b/playground-app/desktop-app/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode plugins { id("org.publicvalue.convention.kotlin.multiplatform") @@ -6,6 +7,7 @@ plugins { } kotlin { + explicitApi = ExplicitApiMode.Disabled jvmToolchain(17) jvm() sourceSets { diff --git a/playground-app/domain/build.gradle.kts b/playground-app/domain/build.gradle.kts index c68067e8..60c2b405 100644 --- a/playground-app/domain/build.gradle.kts +++ b/playground-app/domain/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode import org.publicvalue.convention.addKspDependencyForAllTargets plugins { @@ -5,6 +6,7 @@ plugins { } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/domain/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/domain/Client+createOidcClient.kt b/playground-app/domain/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/domain/Client+createOidcClient.kt index 1365f595..8cda017e 100644 --- a/playground-app/domain/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/domain/Client+createOidcClient.kt +++ b/playground-app/domain/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/domain/Client+createOidcClient.kt @@ -24,7 +24,7 @@ fun Client.createOidcClient(idp: Identityprovider): OpenIdConnectClient { private fun CodeChallengeMethod.toLibrary(): org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod { return when (this) { CodeChallengeMethod.S256 -> org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod.S256 - CodeChallengeMethod.plain -> org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod.plain - CodeChallengeMethod.off -> org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod.off + CodeChallengeMethod.plain -> org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod.PLAIN + CodeChallengeMethod.off -> org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod.OFF } } diff --git a/playground-app/domain/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/domain/IdentityProvider+updateWith.kt b/playground-app/domain/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/domain/IdentityProvider+updateWith.kt index 7f43acbd..e617060e 100644 --- a/playground-app/domain/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/domain/IdentityProvider+updateWith.kt +++ b/playground-app/domain/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/domain/IdentityProvider+updateWith.kt @@ -5,10 +5,10 @@ import org.publicvalue.multiplatform.oidc.types.remote.OpenIdConnectConfiguratio fun Identityprovider.updateWith(config: OpenIdConnectConfiguration): Identityprovider = copy( - endpointToken = config.token_endpoint, - endpointAuthorization = config.authorization_endpoint, - endpointDeviceAuthorization = config.device_authorization_endpoint, - endpointIntrospection = config.introspection_endpoint, - endpointUserInfo = config.userinfo_endpoint, - endpointEndSession = config.end_session_endpoint + endpointToken = config.tokenEndpoint, + endpointAuthorization = config.authorizationEndpoint, + endpointDeviceAuthorization = config.deviceAuthorizationEndpoint, + endpointIntrospection = config.introspectionEndpoint, + endpointUserInfo = config.userinfoEndpoint, + endpointEndSession = config.endSessionEndpoint ) diff --git a/playground-app/shared/build.gradle.kts b/playground-app/shared/build.gradle.kts index 67ece412..d6ef2e4d 100644 --- a/playground-app/shared/build.gradle.kts +++ b/playground-app/shared/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode import org.publicvalue.convention.addKspDependencyForAllTargets plugins { @@ -7,6 +8,7 @@ plugins { } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/ui/clientdetail/build.gradle.kts b/playground-app/ui/clientdetail/build.gradle.kts index 71120042..bd8ff5c6 100644 --- a/playground-app/ui/clientdetail/build.gradle.kts +++ b/playground-app/ui/clientdetail/build.gradle.kts @@ -1,9 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") id("org.publicvalue.convention.compose.multiplatform") } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/ui/clientdetail/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/clientdetail/ClientDetail.kt b/playground-app/ui/clientdetail/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/clientdetail/ClientDetail.kt index 7e9ff635..d3a0397c 100644 --- a/playground-app/ui/clientdetail/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/clientdetail/ClientDetail.kt +++ b/playground-app/ui/clientdetail/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/clientdetail/ClientDetail.kt @@ -346,15 +346,15 @@ internal fun AuthFlow( if (tokenResponse != null) { ExpandableInfo( label = "Access Token", - text = tokenResponse.access_token, + text = tokenResponse.accessToken, ) ExpandableInfo( label = "Refresh Token", - text = tokenResponse.refresh_token.orEmpty(), + text = tokenResponse.refreshToken.orEmpty(), ) ExpandableInfo( label = "Id Token", - text = tokenResponse.id_token.orEmpty(), + text = tokenResponse.idToken.orEmpty(), ) // FormHeadline(text = "Access Token: ") // FormHeadline(text = "Refresh Token: ${tokenResponse.refresh_token}") @@ -412,19 +412,19 @@ fun ExpandableInfo( fun AccessTokenResponse.format(): String { return """ - access_token: $access_token - refresh_token: $refresh_token + access_token: $accessToken + refresh_token: $refreshToken scope: $scope - expires_in: $expires_in - token_type: $token_type + expires_in: $expiresIn + token_type: $tokenType """.trimIndent() } fun ErrorResponse.format(): String { return """ error: $error - error_description: $error_description - error_uri: $error_uri + error_description: $errorDescription + error_uri: $errorUri state: $state """.trimIndent() } diff --git a/playground-app/ui/clientdetail/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/clientdetail/ClientDetailPresenter.kt b/playground-app/ui/clientdetail/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/clientdetail/ClientDetailPresenter.kt index 0f7e8b65..e72e537a 100644 --- a/playground-app/ui/clientdetail/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/clientdetail/ClientDetailPresenter.kt +++ b/playground-app/ui/clientdetail/src/commonMain/kotlin/org/publicvalue/multiplatform/oauth/clientdetail/ClientDetailPresenter.kt @@ -167,7 +167,7 @@ class ClientDetailPresenter( client?.let { client -> if (client.use_webflow_logout) { scope.launch { - logoutWebFlow(client, idToken = tokenResponse?.id_token.orEmpty()) + logoutWebFlow(client, idToken = tokenResponse?.idToken.orEmpty()) .collect { when (it) { is EndSessionResult.Request -> { @@ -183,7 +183,7 @@ class ClientDetailPresenter( } } else { scope.launch { - logoutPost(client, idToken = tokenResponse?.id_token.orEmpty()) + logoutPost(client, idToken = tokenResponse?.idToken.orEmpty()) .collect { when (it) { is EndSessionResult.Request -> { diff --git a/playground-app/ui/clientlist/build.gradle.kts b/playground-app/ui/clientlist/build.gradle.kts index b2fef861..d5cbb540 100644 --- a/playground-app/ui/clientlist/build.gradle.kts +++ b/playground-app/ui/clientlist/build.gradle.kts @@ -1,9 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") id("org.publicvalue.convention.compose.multiplatform") } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/ui/common/build.gradle.kts b/playground-app/ui/common/build.gradle.kts index 401a545c..587e8004 100644 --- a/playground-app/ui/common/build.gradle.kts +++ b/playground-app/ui/common/build.gradle.kts @@ -1,9 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") id("org.publicvalue.convention.compose.multiplatform") } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/ui/idplist/build.gradle.kts b/playground-app/ui/idplist/build.gradle.kts index 5403f27a..06d5093f 100644 --- a/playground-app/ui/idplist/build.gradle.kts +++ b/playground-app/ui/idplist/build.gradle.kts @@ -1,9 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") id("org.publicvalue.convention.compose.multiplatform") } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/playground-app/ui/root/build.gradle.kts b/playground-app/ui/root/build.gradle.kts index e1d9bc88..6f95877d 100644 --- a/playground-app/ui/root/build.gradle.kts +++ b/playground-app/ui/root/build.gradle.kts @@ -1,9 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("org.publicvalue.convention.kotlin.multiplatform") id("org.publicvalue.convention.compose.multiplatform") } kotlin { + explicitApi = ExplicitApiMode.Disabled jvm() sourceSets { val commonMain by getting { diff --git a/sample-app/android-app/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/MainActivity.kt b/sample-app/android-app/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/MainActivity.kt index 73c5ad4e..357180dd 100644 --- a/sample-app/android-app/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/MainActivity.kt +++ b/sample-app/android-app/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/MainActivity.kt @@ -6,8 +6,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import org.publicvalue.multiplatform.oidc.appsupport.AndroidCodeAuthFlowFactory - -class MainActivity : ComponentActivity() { +internal class MainActivity : ComponentActivity() { // There should only be one instance of this factory. // The flow should also be created and started from an @@ -24,4 +23,4 @@ class MainActivity : ComponentActivity() { ) } } -} \ No newline at end of file +} diff --git a/sample-app/build.gradle.kts b/sample-app/build.gradle.kts index 5ea84dd6..0cb0074a 100644 --- a/sample-app/build.gradle.kts +++ b/sample-app/build.gradle.kts @@ -5,4 +5,5 @@ plugins { alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.kmp) apply false + alias(libs.plugins.custom.detekt) } diff --git a/sample-app/config/detekt/detekt.yml b/sample-app/config/detekt/detekt.yml new file mode 100644 index 00000000..e09309ff --- /dev/null +++ b/sample-app/config/detekt/detekt.yml @@ -0,0 +1,1081 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: false + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [] + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + ignoreAnnotated: [ 'Composable' ] + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: false + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + - 'wasmJs' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' + +formatting: + active: true + android: false + autoCorrect: true + AnnotationOnSeparateLine: + active: true + autoCorrect: true + indentSize: 4 + AnnotationSpacing: + active: true + autoCorrect: true + ArgumentListWrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + BlockCommentInitialStarAlignment: + active: true + autoCorrect: true + ChainWrapping: + active: true + autoCorrect: true + indentSize: 4 + ClassName: + active: false + CommentSpacing: + active: true + autoCorrect: true + CommentWrapping: + active: true + autoCorrect: true + indentSize: 4 + ContextReceiverMapping: + active: false + autoCorrect: true + maxLineLength: 120 + indentSize: 4 + DiscouragedCommentLocation: + active: false + autoCorrect: true + EnumEntryNameCase: + active: true + autoCorrect: true + EnumWrapping: + active: false + autoCorrect: true + indentSize: 4 + Filename: + active: true + FinalNewline: + active: true + autoCorrect: true + insertFinalNewLine: true + FunKeywordSpacing: + active: true + autoCorrect: true + FunctionName: + active: false + FunctionReturnTypeSpacing: + active: true + autoCorrect: true + maxLineLength: 120 + FunctionSignature: + active: false + autoCorrect: true + forceMultilineWhenParameterCountGreaterOrEqualThan: 2147483647 + functionBodyExpressionWrapping: 'default' + maxLineLength: 120 + indentSize: 4 + FunctionStartOfBodySpacing: + active: true + autoCorrect: true + FunctionTypeReferenceSpacing: + active: true + autoCorrect: true + IfElseBracing: + active: false + autoCorrect: true + indentSize: 4 + IfElseWrapping: + active: false + autoCorrect: true + indentSize: 4 + ImportOrdering: + active: true + autoCorrect: true + layout: '*,java.**,javax.**,kotlin.**,^' + Indentation: + active: true + autoCorrect: true + indentSize: 4 + KdocWrapping: + active: true + autoCorrect: true + indentSize: 4 + MaximumLineLength: + active: true + maxLineLength: 120 + ignoreBackTickedIdentifier: false + ModifierListSpacing: + active: true + autoCorrect: true + ModifierOrdering: + active: true + autoCorrect: true + MultiLineIfElse: + active: true + autoCorrect: true + indentSize: 4 + MultilineExpressionWrapping: + active: false + autoCorrect: true + indentSize: 4 + NoBlankLineBeforeRbrace: + active: true + autoCorrect: true + NoBlankLineInList: + active: false + autoCorrect: true + NoBlankLinesInChainedMethodCalls: + active: true + autoCorrect: true + NoConsecutiveBlankLines: + active: true + autoCorrect: true + NoConsecutiveComments: + active: false + NoEmptyClassBody: + active: true + autoCorrect: true + NoEmptyFirstLineInClassBody: + active: false + autoCorrect: true + indentSize: 4 + NoEmptyFirstLineInMethodBlock: + active: true + autoCorrect: true + NoLineBreakAfterElse: + active: true + autoCorrect: true + NoLineBreakBeforeAssignment: + active: true + autoCorrect: true + NoMultipleSpaces: + active: true + autoCorrect: true + NoSemicolons: + active: true + autoCorrect: true + NoSingleLineBlockComment: + active: false + autoCorrect: true + indentSize: 4 + NoTrailingSpaces: + active: true + autoCorrect: true + NoUnitReturn: + active: true + autoCorrect: true + NoUnusedImports: + active: true + autoCorrect: true + NoWildcardImports: + active: true + packagesToUseImportOnDemandProperty: 'java.util.*,kotlinx.android.synthetic.**' + NullableTypeSpacing: + active: true + autoCorrect: true + PackageName: + active: true + autoCorrect: true + ParameterListSpacing: + active: false + autoCorrect: true + ParameterListWrapping: + active: true + autoCorrect: true + maxLineLength: 120 + indentSize: 4 + ParameterWrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + PropertyName: + active: false + PropertyWrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + SpacingAroundAngleBrackets: + active: true + autoCorrect: true + SpacingAroundColon: + active: true + autoCorrect: true + SpacingAroundComma: + active: true + autoCorrect: true + SpacingAroundCurly: + active: true + autoCorrect: true + SpacingAroundDot: + active: true + autoCorrect: true + SpacingAroundDoubleColon: + active: true + autoCorrect: true + SpacingAroundKeyword: + active: true + autoCorrect: true + SpacingAroundOperators: + active: true + autoCorrect: true + SpacingAroundParens: + active: true + autoCorrect: true + SpacingAroundRangeOperator: + active: true + autoCorrect: true + SpacingAroundUnaryOperator: + active: true + autoCorrect: true + SpacingBetweenDeclarationsWithAnnotations: + active: true + autoCorrect: true + SpacingBetweenDeclarationsWithComments: + active: true + autoCorrect: true + SpacingBetweenFunctionNameAndOpeningParenthesis: + active: true + autoCorrect: true + StringTemplate: + active: true + autoCorrect: true + StringTemplateIndent: + active: false + autoCorrect: true + indentSize: 4 + TrailingCommaOnCallSite: + active: false + autoCorrect: true + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: false + autoCorrect: true + useTrailingCommaOnDeclarationSite: true + TryCatchFinallySpacing: + active: false + autoCorrect: true + indentSize: 4 + TypeArgumentListSpacing: + active: false + autoCorrect: true + indentSize: 4 + TypeParameterListSpacing: + active: false + autoCorrect: true + indentSize: 4 + UnnecessaryParenthesesBeforeTrailingLambda: + active: true + autoCorrect: true + Wrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + +libraries: + active: true + ForbiddenPublicDataClass: + active: true + ignorePackages: + - '*.internal' + - '*.internal.*' + LibraryCodeMustSpecifyReturnType: + active: true + allowOmitUnit: false + LibraryEntitiesShouldNotBePublic: + active: false diff --git a/sample-app/desktop-app/src/jvmMain/kotlin/SampleDesktopApp.kt b/sample-app/desktop-app/src/jvmMain/kotlin/SampleDesktopApp.kt index c2e42b28..8b6bac2b 100644 --- a/sample-app/desktop-app/src/jvmMain/kotlin/SampleDesktopApp.kt +++ b/sample-app/desktop-app/src/jvmMain/kotlin/SampleDesktopApp.kt @@ -5,8 +5,7 @@ import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -fun main() = application { - +internal fun main() = application { val state = rememberWindowState( placement = WindowPlacement.Floating, size = DpSize(width = 800.dp, height = 800.dp) @@ -17,7 +16,6 @@ fun main() = application { state = state, onCloseRequest = ::exitApplication ) { - MainView() } -} \ No newline at end of file +} diff --git a/sample-app/ios-app/iosApp.xcodeproj/project.pbxproj b/sample-app/ios-app/iosApp.xcodeproj/project.pbxproj index 5027ebc9..98f7d45c 100644 --- a/sample-app/ios-app/iosApp.xcodeproj/project.pbxproj +++ b/sample-app/ios-app/iosApp.xcodeproj/project.pbxproj @@ -58,7 +58,6 @@ 7555FF7D242A565900829871 /* iosApp */, 7555FF7C242A565900829871 /* Products */, 42799AB246E5F90AF97AA0EF /* Frameworks */, - BB851A233843371257137ED8 /* xcuserdata */, ); sourceTree = ""; }; @@ -91,30 +90,6 @@ path = Configuration; sourceTree = ""; }; - BB851919F038E80F7B4E422D /* julian.xcuserdatad */ = { - isa = PBXGroup; - children = ( - BB851E7AE6DB451561E7AD70 /* xcschemes */, - ); - path = julian.xcuserdatad; - sourceTree = ""; - }; - BB851A233843371257137ED8 /* xcuserdata */ = { - isa = PBXGroup; - children = ( - BB851919F038E80F7B4E422D /* julian.xcuserdatad */, - ); - name = xcuserdata; - path = iosApp.xcodeproj/xcuserdata; - sourceTree = ""; - }; - BB851E7AE6DB451561E7AD70 /* xcschemes */ = { - isa = PBXGroup; - children = ( - ); - path = xcschemes; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ diff --git a/sample-app/ios-app/iosApp/Readme.swift b/sample-app/ios-app/iosApp/Readme.swift index 4c82003f..4f51ff92 100644 --- a/sample-app/ios-app/iosApp/Readme.swift +++ b/sample-app/ios-app/iosApp/Readme.swift @@ -61,18 +61,18 @@ struct Readme { // Request access token using code auth flow: func _2() async { - let flow = CodeAuthFlow(client: client) - do { - let tokens = try await flow.getAccessToken() - } catch { - print(error) - } + // let flow = CodeAuthFlow(client: client) + // do { + // let tokens = try await flow.getAccessToken() + // } catch { + // print(error) + // } } // Perform refresh or endSession: func _3() async throws { - try await client.refreshToken(refreshToken: tokens.refresh_token!) - try await client.endSession(idToken: tokens.id_token!) + try await client.refreshToken(refreshToken: tokens.refreshToken!) + try await client.endSession(idToken: tokens.idToken!) } // customize endSession request: @@ -82,28 +82,28 @@ struct Readme { requestBuilder.url.parameters.append(name: "custom_parameter", value: "value") } // endSession with Web flow (opens browser and handles post_logout_redirect_uri redirect) - let flow = CodeAuthFlow(client: client) - try await flow.endSession(idToken: "", configureEndSessionUrl: { urlBuilder in - }) + // let flow = CodeAuthFlow(client: client) + // try await flow.endSession(idToken: "", configureEndSessionUrl: { urlBuilder in + // }) } // customize getAccessToken request: func _3b() async throws { - let flow = CodeAuthFlow(client: client) - try await flow.getAccessToken( - configureAuthUrl: { urlBuilder in - urlBuilder.parameters.append(name: "prompt", value: "login") - }, - configureTokenExchange: { requestBuilder in - requestBuilder.headers.append(name: "additionalHeaderField", value: "value") - } - ) + // let flow = CodeAuthFlow(client: client) + // try await flow.getAccessToken( + // configureAuthUrl: { urlBuilder in + // urlBuilder.parameters.append(name: "prompt", value: "login") + // }, + // configureTokenExchange: { requestBuilder in + // requestBuilder.headers.append(name: "additionalHeaderField", value: "value") + // } + // ) } // We provide simple JWT parsing: func _4() { - let jwt = tokens.id_token.map { try! JwtParser.shared.parse(from: $0) } + let jwt = tokens.idToken.map { try! JwtParser.shared.parse(from: $0) } print(jwt?.payload.aud) // print audience print(jwt?.payload.iss) // print issuer print(jwt?.payload.additionalClaims["email"]) // get claim diff --git a/sample-app/settings/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/settings/AndroidSettingsStore.kt b/sample-app/settings/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/settings/AndroidSettingsStore.kt index e54f9f8c..d9c08889 100644 --- a/sample-app/settings/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/settings/AndroidSettingsStore.kt +++ b/sample-app/settings/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/settings/AndroidSettingsStore.kt @@ -4,11 +4,13 @@ import android.content.Context import com.russhwolf.settings.SharedPreferencesSettings import com.russhwolf.settings.get -class AndroidSettingsStore( +public class AndroidSettingsStore( context: Context ) : SettingsStore { - val settings = SharedPreferencesSettings(context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)) + private val settings = SharedPreferencesSettings( + context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + ) override suspend fun get(key: String): String? { return settings[key] } @@ -24,5 +26,4 @@ class AndroidSettingsStore( override suspend fun clear() { settings.clear() } - -} \ No newline at end of file +} diff --git a/sample-app/settings/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/settings/SettingsStore.kt b/sample-app/settings/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/settings/SettingsStore.kt index 30ad08ac..a1a108c1 100644 --- a/sample-app/settings/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/settings/SettingsStore.kt +++ b/sample-app/settings/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/settings/SettingsStore.kt @@ -1,8 +1,8 @@ package org.publicvalue.multiplatform.oidc.settings -interface SettingsStore { - suspend fun get(key: String): String? - suspend fun put(key: String, value: String) - suspend fun remove(key: String) - suspend fun clear() -} \ No newline at end of file +public interface SettingsStore { + public suspend fun get(key: String): String? + public suspend fun put(key: String, value: String) + public suspend fun remove(key: String) + public suspend fun clear() +} diff --git a/sample-app/settings/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/settings/IosSettingsStore.kt b/sample-app/settings/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/settings/IosSettingsStore.kt index 55f8f9d8..ef6cb3b6 100644 --- a/sample-app/settings/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/settings/IosSettingsStore.kt +++ b/sample-app/settings/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/settings/IosSettingsStore.kt @@ -11,7 +11,7 @@ import platform.Security.kSecAttrAccessibleAfterFirstUnlock import platform.Security.kSecAttrService @OptIn(ExperimentalSettingsImplementation::class) -class IosSettingsStore : SettingsStore { +public class IosSettingsStore : SettingsStore { @OptIn(ExperimentalForeignApi::class) private val keyChainSettings by lazy { @@ -36,6 +36,4 @@ class IosSettingsStore : SettingsStore { override suspend fun clear() { keyChainSettings.clear() } - - -} \ No newline at end of file +} diff --git a/sample-app/settings/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/settings/JvmSettingsStore.kt b/sample-app/settings/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/settings/JvmSettingsStore.kt index 008c6221..24e2030c 100644 --- a/sample-app/settings/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/settings/JvmSettingsStore.kt +++ b/sample-app/settings/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/settings/JvmSettingsStore.kt @@ -4,9 +4,9 @@ import com.russhwolf.settings.PreferencesSettings import com.russhwolf.settings.get import java.util.prefs.Preferences -class JvmSettingsStore: SettingsStore { +public class JvmSettingsStore : SettingsStore { - val prefs = PreferencesSettings(Preferences.userRoot()) + private val prefs = PreferencesSettings(Preferences.userRoot()) override suspend fun get(key: String): String? { return prefs[key] @@ -23,5 +23,4 @@ class JvmSettingsStore: SettingsStore { override suspend fun clear() { prefs.clear() } - -} \ No newline at end of file +} diff --git a/sample-app/settings/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/settings/WasmJsSettingsStore.kt b/sample-app/settings/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/settings/WasmJsSettingsStore.kt index 3231b107..90d6017b 100644 --- a/sample-app/settings/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/settings/WasmJsSettingsStore.kt +++ b/sample-app/settings/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/settings/WasmJsSettingsStore.kt @@ -5,16 +5,15 @@ import com.russhwolf.settings.get import kotlinx.browser.localStorage import org.w3c.dom.Storage -class WasmJsSettingsStore: SettingsStore { +public class WasmJsSettingsStore : SettingsStore { - val prefs: StorageSettings + private val prefs: StorageSettings - constructor(storage: Storage) { + public constructor(storage: Storage) { prefs = StorageSettings(storage) } - constructor(): this(localStorage) - + public constructor() : this(localStorage) override suspend fun get(key: String): String? { return prefs[key] @@ -31,5 +30,4 @@ class WasmJsSettingsStore: SettingsStore { override suspend fun clear() { prefs.clear() } - -} \ No newline at end of file +} diff --git a/sample-app/shared/code-quality/baseline.xml b/sample-app/shared/code-quality/baseline.xml new file mode 100644 index 00000000..08e19b35 --- /dev/null +++ b/sample-app/shared/code-quality/baseline.xml @@ -0,0 +1,29 @@ + + + + + CyclomaticComplexMethod:ConfigPresenter.kt$ConfigPresenter$@Composable override fun present(): ConfigUiState + CyclomaticComplexMethod:HomePresenter.kt$HomePresenter$@Composable override fun present(): HomeUiState + Filename:Constants.android.kt$org.publicvalue.multiplatform.oidc.sample.Constants.android.kt + Filename:Constants.ios.kt$org.publicvalue.multiplatform.oidc.sample.Constants.ios.kt + Filename:Constants.jvm.kt$org.publicvalue.multiplatform.oidc.sample.Constants.jvm.kt + Filename:Constants.wasmJs.kt$org.publicvalue.multiplatform.oidc.sample.Constants.wasmJs.kt + Filename:Parcelize.android.kt$org.publicvalue.multiplatform.oidc.sample.screens.Parcelize.android.kt + Filename:Parcelize.ios.kt$org.publicvalue.multiplatform.oidc.sample.screens.Parcelize.ios.kt + Filename:Parcelize.jvm.kt$org.publicvalue.multiplatform.oidc.sample.screens.Parcelize.jvm.kt + Filename:Parcelize.wasmJs.kt$org.publicvalue.multiplatform.oidc.sample.screens.Parcelize.wasmJs.kt + Filename:main.android.kt$.main.android.kt + Filename:main.desktop.kt$.main.desktop.kt + Filename:main.ios.kt$.main.ios.kt + Filename:main.wasmJs.kt$.main.wasmJs.kt + LongMethod:Config.kt$@Composable internal fun Config( modifier: Modifier = Modifier, discoveryUrl: String?, authEndpoint: String?, tokenEndpoint: String?, endSessionEndpoint: String?, clientId: String?, clientSecret: String?, scope: String?, challengeMethod: CodeChallengeMethod?, onChangeDiscoveryUrl: (String) -> Unit, onChangeAuthEndpoint: (String) -> Unit, onChangeTokenEndpoint: (String) -> Unit, onChangeEndSessionEndpoint: (String) -> Unit, onChangeClientId: (String) -> Unit, onChangeClientSecret: (String) -> Unit, onChangeScope: (String) -> Unit, onChangeChallengeMethod: (CodeChallengeMethod) -> Unit, onClickDiscover: () -> Unit ) + LongMethod:Config.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun Config( state: ConfigUiState, modifier: Modifier = Modifier ) + LongMethod:ConfigPresenter.kt$ConfigPresenter$fun eventSink(event: ConfigUiEvent) + LongMethod:Home.kt$@Composable internal fun Home( modifier: Modifier = Modifier, loginEnabled: Boolean, refreshEnabled: Boolean, logoutEnabled: Boolean, onLoginClick: () -> Unit, onLogoutClick: (Boolean) -> Unit, onRefreshClick: () -> Unit, tokenData: TokenData?, subject: String?, errorMessage: String? ) + LongMethod:HomePresenter.kt$HomePresenter$fun eventSink(event: HomeUiEvent) + LongParameterList:Config.kt$( modifier: Modifier = Modifier, discoveryUrl: String?, authEndpoint: String?, tokenEndpoint: String?, endSessionEndpoint: String?, clientId: String?, clientSecret: String?, scope: String?, challengeMethod: CodeChallengeMethod?, onChangeDiscoveryUrl: (String) -> Unit, onChangeAuthEndpoint: (String) -> Unit, onChangeTokenEndpoint: (String) -> Unit, onChangeEndSessionEndpoint: (String) -> Unit, onChangeClientId: (String) -> Unit, onChangeClientSecret: (String) -> Unit, onChangeScope: (String) -> Unit, onChangeChallengeMethod: (CodeChallengeMethod) -> Unit, onClickDiscover: () -> Unit ) + LongParameterList:Home.kt$( modifier: Modifier = Modifier, loginEnabled: Boolean, refreshEnabled: Boolean, logoutEnabled: Boolean, onLoginClick: () -> Unit, onLogoutClick: (Boolean) -> Unit, onRefreshClick: () -> Unit, tokenData: TokenData?, subject: String?, errorMessage: String? ) + TooGenericExceptionCaught:ErrorPresenter.kt$t: Throwable + UnusedParameter:Config.kt$modifier: Modifier = Modifier + + diff --git a/sample-app/shared/src/androidMain/kotlin/ReadmeAndroid.kt b/sample-app/shared/src/androidMain/kotlin/ReadmeAndroid.kt index 77e606a9..251b008c 100644 --- a/sample-app/shared/src/androidMain/kotlin/ReadmeAndroid.kt +++ b/sample-app/shared/src/androidMain/kotlin/ReadmeAndroid.kt @@ -6,15 +6,16 @@ import org.publicvalue.multiplatform.oidc.tokenstore.TokenRefreshHandler import org.publicvalue.multiplatform.oidc.tokenstore.TokenStore @OptIn(ExperimentalOpenIdConnect::class) -object ReadmeAndroid { +@Suppress("unused") +internal object ReadmeAndroid { - val client = OpenIdConnectClient { } + val client = OpenIdConnectClient { } val tokenStore: TokenStore = TODO() val token: String = TODO() val refreshHandler: TokenRefreshHandler = TODO() // okhttp - suspend fun `okhttp`() { + fun okhttp() { val authenticator = OpenIdConnectAuthenticator { getAccessToken { tokenStore.getAccessToken() } refreshTokens { oldAccessToken -> refreshHandler.refreshAndSaveToken(client, oldAccessToken) } diff --git a/sample-app/shared/src/androidMain/kotlin/main.android.kt b/sample-app/shared/src/androidMain/kotlin/main.android.kt index 0a2d77db..c695636b 100644 --- a/sample-app/shared/src/androidMain/kotlin/main.android.kt +++ b/sample-app/shared/src/androidMain/kotlin/main.android.kt @@ -7,7 +7,7 @@ import org.publicvalue.multiplatform.oidc.sample.screens.HomeScreen import org.publicvalue.multiplatform.oidc.settings.AndroidSettingsStore @Composable -fun MainView( +public fun MainView( authFlowFactory: AndroidCodeAuthFlowFactory ) { val context = LocalContext.current @@ -15,7 +15,6 @@ fun MainView( val backstack = rememberSaveableBackStack(initialScreens = listOf(HomeScreen)) val navigator = rememberCircuitNavigator(backstack) { - } val settingsStore = AndroidSettingsStore( diff --git a/sample-app/shared/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.android.kt b/sample-app/shared/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.android.kt index 35c99d5c..d35049a5 100644 --- a/sample-app/shared/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.android.kt +++ b/sample-app/shared/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.android.kt @@ -1,6 +1,6 @@ package org.publicvalue.multiplatform.oidc.sample -actual object PlatformConstants : Constants { +internal actual object PlatformConstants : Constants { override val redirectUrl: String = "org.publicvalue.multiplatform.oidc.sample://redirect" override val postLogoutRedirectUrl: String = "org.publicvalue.multiplatform.oidc.sample://logout" -} \ No newline at end of file +} diff --git a/sample-app/shared/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.android.kt b/sample-app/shared/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.android.kt index 93a1bc75..93e9bbd7 100644 --- a/sample-app/shared/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.android.kt +++ b/sample-app/shared/src/androidMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.android.kt @@ -1,3 +1,3 @@ package org.publicvalue.multiplatform.oidc.sample.screens -actual typealias CommonParcelable = android.os.Parcelable \ No newline at end of file +internal actual typealias CommonParcelable = android.os.Parcelable diff --git a/sample-app/shared/src/commonMain/kotlin/App.kt b/sample-app/shared/src/commonMain/kotlin/App.kt index 6d920722..465ac571 100644 --- a/sample-app/shared/src/commonMain/kotlin/App.kt +++ b/sample-app/shared/src/commonMain/kotlin/App.kt @@ -3,23 +3,22 @@ import com.slack.circuit.backstack.SaveableBackStack import com.slack.circuit.foundation.Circuit import com.slack.circuit.runtime.Navigator import org.publicvalue.multiplatform.oidc.appsupport.CodeAuthFlowFactory -import org.publicvalue.multiplatform.oidc.settings.SettingsStore import org.publicvalue.multiplatform.oidc.sample.Root +import org.publicvalue.multiplatform.oidc.sample.circuit.UiFactories.Companion.factories import org.publicvalue.multiplatform.oidc.sample.circuit.UiFactories.Companion.presenterFactories -import org.publicvalue.multiplatform.oidc.sample.circuit.UiFactories.Companion.uiFactories +import org.publicvalue.multiplatform.oidc.settings.SettingsStore @Composable -fun App( +internal fun App( backstack: SaveableBackStack, navigator: Navigator, settingsStore: SettingsStore, authFlowFactory: CodeAuthFlowFactory ) { - val circuit = Circuit.Builder() - .addUiFactories(uiFactories) - .addPresenterFactories(presenterFactories(authFlowFactory)) - .build() + .addUiFactories(factories) + .addPresenterFactories(presenterFactories(authFlowFactory)) + .build() Root( circuit = circuit, @@ -27,4 +26,4 @@ fun App( navigator = navigator, settingsStore = settingsStore, ) -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.kt index f36268e6..099ef2d3 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.kt @@ -1,8 +1,8 @@ package org.publicvalue.multiplatform.oidc.sample -interface Constants { +internal interface Constants { val redirectUrl: String val postLogoutRedirectUrl: String } -expect object PlatformConstants: Constants \ No newline at end of file +internal expect object PlatformConstants : Constants diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Root.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Root.kt index c412725e..a86dbbcd 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Root.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Root.kt @@ -22,13 +22,12 @@ import org.publicvalue.multiplatform.oidc.sample.data.LocalSettingsStore import org.publicvalue.multiplatform.oidc.sample.data.OidcSettingsStore import org.publicvalue.multiplatform.oidc.settings.SettingsStore - -//val DMANavigator: Navigator = remember(navigator) { +// val DMANavigator: Navigator = remember(navigator) { // OAuthPlaygroundNavigator(navigator, backstack, onOpenUrl, logger) -//} +// } @Composable -fun Root( +internal fun Root( circuit: Circuit, navigator: Navigator, backstack: SaveableBackStack, @@ -39,26 +38,26 @@ fun Root( bottomBar = { }, contentWindowInsets = ScaffoldDefaults.contentWindowInsets - .exclude(WindowInsets.statusBars) - // .exclude(WindowInsets.navigationBars), - ) { paddingValues -> - Surface( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - Row { - CompositionLocalProvider(LocalSettingsStore provides oidcSettingsStore) { - NavigableCircuitContent( - circuit = circuit, - navigator = navigator, - backStack = backstack, - modifier = Modifier - .weight(1f) - .fillMaxHeight(), - ) - } + .exclude(WindowInsets.statusBars) + // .exclude(WindowInsets.navigationBars), + ) { paddingValues -> + Surface( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Row { + CompositionLocalProvider(LocalSettingsStore provides oidcSettingsStore) { + NavigableCircuitContent( + circuit = circuit, + navigator = navigator, + backStack = backstack, + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + ) } } } -} \ No newline at end of file + } +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/circuit/ErrorPresenter.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/circuit/ErrorPresenter.kt index 398b1c34..65c5791a 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/circuit/ErrorPresenter.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/circuit/ErrorPresenter.kt @@ -4,7 +4,7 @@ import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.presenter.Presenter import kotlinx.coroutines.flow.MutableStateFlow -interface ErrorPresenter: Presenter { +internal interface ErrorPresenter : Presenter { var errorMessage: MutableStateFlow fun resetErrorMessage() { @@ -16,12 +16,10 @@ interface ErrorPresenter: Presenter { } } -suspend fun , UiState> T.catchErrorMessage(block: suspend T.() -> Unit) { +internal suspend fun , UiState> T.catchErrorMessage(block: suspend T.() -> Unit) { try { block() } catch (t: Throwable) { - println("printing trace") - t.printStackTrace() errorMessage.value = t.message } } diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/circuit/UiFactories.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/circuit/UiFactories.kt index 2c7bd397..b6536cfb 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/circuit/UiFactories.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/circuit/UiFactories.kt @@ -6,16 +6,16 @@ import org.publicvalue.multiplatform.oidc.sample.config.ConfigUiFactory import org.publicvalue.multiplatform.oidc.sample.home.HomePresenterFactory import org.publicvalue.multiplatform.oidc.sample.home.HomeUiFactory - -class UiFactories { +internal class UiFactories internal constructor() { companion object { - val uiFactories = listOf( - HomeUiFactory, ConfigUiFactory + val factories = listOf( + HomeUiFactory, + ConfigUiFactory ) fun presenterFactories(authFlowFactory: CodeAuthFlowFactory) = listOf( - HomePresenterFactory(authFlowFactory), ConfigPresenterFactory + HomePresenterFactory(authFlowFactory), + ConfigPresenterFactory ) } } - diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/Config.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/Config.kt index 0fe02431..38eb2d92 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/Config.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/Config.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -31,15 +30,10 @@ import org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Config( +internal fun Config( state: ConfigUiState, modifier: Modifier = Modifier ) { - var greetingText by remember { mutableStateOf("Hello, World!") } - var showImage by remember { mutableStateOf(false) } - -// val settingsStore = LocalSettingsStore.current - Scaffold( topBar = { TopAppBar( @@ -60,10 +54,10 @@ fun Config( authEndpoint = state.idpSettings.endpointAuthorization, tokenEndpoint = state.idpSettings.endpointToken, endSessionEndpoint = state.idpSettings.endpointEndSession, - clientId = state.clientSettings.client_id, - clientSecret = state.clientSettings.client_secret, + clientId = state.clientSettings.clientId, + clientSecret = state.clientSettings.clientSecret, scope = state.clientSettings.scope, - challengeMethod = state.clientSettings.code_challenge_method, + challengeMethod = state.clientSettings.codeChallengeMethod, onChangeAuthEndpoint = { state.eventSink( ConfigUiEvent.ChangeEndpointAuthorization(it) @@ -114,7 +108,7 @@ fun Config( } @Composable -fun Config( +internal fun Config( modifier: Modifier = Modifier, discoveryUrl: String?, authEndpoint: String?, @@ -134,10 +128,11 @@ fun Config( onChangeChallengeMethod: (CodeChallengeMethod) -> Unit, onClickDiscover: () -> Unit ) { - Column(modifier - .fillMaxWidth() - .padding(bottom = 8.dp) - .verticalScroll(rememberScrollState()), + Column( + modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { var discoveryUrl by remember(discoveryUrl == null) { @@ -168,7 +163,10 @@ fun Config( FormHeadline(text = "IDP") SingleLineInput( value = discoveryUrl, - onValueChange = { discoveryUrl = it; onChangeDiscoveryUrl(it) }, + onValueChange = { + discoveryUrl = it + onChangeDiscoveryUrl(it) + }, label = { Text("Discovery URL") } ) TextButton(onClick = { onClickDiscover() }) { @@ -177,35 +175,53 @@ fun Config( FormHeadline(text = "Endpoints") SingleLineInput( value = authEndpoint, - onValueChange = { authEndpoint = it; onChangeAuthEndpoint(it)}, + onValueChange = { + authEndpoint = it + onChangeAuthEndpoint(it) + }, label = { Text("Authorization") } ) SingleLineInput( value = tokenEndpoint, - onValueChange = { tokenEndpoint = it; onChangeTokenEndpoint(it)}, + onValueChange = { + tokenEndpoint = it + onChangeTokenEndpoint(it) + }, label = { Text("Token") } ) SingleLineInput( value = endSessionEndpoint, - onValueChange = { endSessionEndpoint = it; onChangeEndSessionEndpoint(it)}, + onValueChange = { + endSessionEndpoint = it + onChangeEndSessionEndpoint(it) + }, label = { Text("End Session") } ) FormHeadline(text = "Client") SingleLineInput( value = clientId, - onValueChange = { clientId = it; onChangeClientId(it)}, + onValueChange = { + clientId = it + onChangeClientId(it) + }, label = { Text("Client ID") } ) SingleLineInput( value = clientSecret, - onValueChange = { clientSecret = it; onChangeClientSecret(it)}, + onValueChange = { + clientSecret = it + onChangeClientSecret(it) + }, label = { Text("Client secret") } ) FormHeadline(text = "Auth code flow request parameters") SingleLineInput( value = scope, - onValueChange = { scope = it; onChangeScope(it)}, + onValueChange = { + scope = it + onChangeScope(it) + }, label = { Text("Scope") } ) FormHeadline(text = "Code challenge method") @@ -222,4 +238,4 @@ fun Config( Text("Note: redirect_url is ${PlatformConstants.redirectUrl}") Text("Note: post_logout_redirect_url is ${PlatformConstants.postLogoutRedirectUrl}") } -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigPresenter.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigPresenter.kt index 889319a8..1c64adfb 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigPresenter.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigPresenter.kt @@ -16,9 +16,9 @@ import org.publicvalue.multiplatform.oidc.sample.domain.ClientSettings import org.publicvalue.multiplatform.oidc.sample.domain.IdpSettings import kotlin.String -class ConfigPresenter( +internal class ConfigPresenter( val navigator: Navigator -): ErrorPresenter { +) : ErrorPresenter { override var errorMessage = MutableStateFlow(null) @@ -31,24 +31,27 @@ class ConfigPresenter( val idpSettings by settingsStore.observeIdpSettings().collectAsRetainedState() fun eventSink(event: ConfigUiEvent) { - when(event) { + when (event) { ConfigUiEvent.NavigateBack -> { - navigator.pop() + navigator.pop() } + is ConfigUiEvent.ChangeClientId -> { val clientSettings = clientSettings ?: ClientSettings.Empty scope.launch { - settingsStore.setClientSettings(clientSettings.copy(client_id = event.clientId)) + settingsStore.setClientSettings(clientSettings.copy(clientId = event.clientId)) } } + is ConfigUiEvent.ChangeClientSecret -> { val clientSettings = clientSettings ?: ClientSettings.Empty scope.launch { - settingsStore.setClientSettings(clientSettings.copy(client_secret = event.clientSecret)) + settingsStore.setClientSettings(clientSettings.copy(clientSecret = event.clientSecret)) } } + is ConfigUiEvent.ChangeScope -> { val clientSettings = clientSettings ?: ClientSettings.Empty @@ -56,13 +59,17 @@ class ConfigPresenter( settingsStore.setClientSettings(clientSettings.copy(scope = event.scope)) } } + is ConfigUiEvent.ChangeCodeChallengeMethod -> { val clientSettings = clientSettings ?: ClientSettings.Empty scope.launch { - settingsStore.setClientSettings(clientSettings.copy(code_challenge_method = event.codeChallengeMethod)) + settingsStore.setClientSettings( + clientSettings.copy(codeChallengeMethod = event.codeChallengeMethod) + ) } } + is ConfigUiEvent.ChangeDiscoveryUrl -> { val idpSettings = idpSettings ?: IdpSettings.Empty @@ -70,13 +77,17 @@ class ConfigPresenter( settingsStore.setIdpSettings(idpSettings.copy(discoveryUrl = event.discoveryUrl)) } } + is ConfigUiEvent.ChangeEndpointAuthorization -> { val idpSettings = idpSettings ?: IdpSettings.Empty scope.launch { - settingsStore.setIdpSettings(idpSettings.copy(endpointAuthorization = event.endpointAuthorization)) + settingsStore.setIdpSettings( + idpSettings.copy(endpointAuthorization = event.endpointAuthorization) + ) } } + is ConfigUiEvent.ChangeEndpointEndSession -> { val idpSettings = idpSettings ?: IdpSettings.Empty @@ -84,6 +95,7 @@ class ConfigPresenter( settingsStore.setIdpSettings(idpSettings.copy(endpointEndSession = event.endpointEndSession)) } } + is ConfigUiEvent.ChangeEndpointToken -> { val idpSettings = idpSettings ?: IdpSettings.Empty @@ -91,10 +103,13 @@ class ConfigPresenter( settingsStore.setIdpSettings(idpSettings.copy(endpointToken = event.endpointToken)) } } + ConfigUiEvent.Discover -> { scope.launch { catchErrorMessage { - if (!idpSettings?.discoveryUrl.isNullOrEmpty() && idpSettings?.discoveryUrl?.let { Url(it) } != null) { + if (!idpSettings?.discoveryUrl.isNullOrEmpty() && + idpSettings?.discoveryUrl?.let { Url(it) } != null + ) { settingsStore.setIdpSettings( IdpSettings( discoveryUrl = idpSettings?.discoveryUrl @@ -105,12 +120,12 @@ class ConfigPresenter( idpSettings.discoveryUrl?.let { val config = d.downloadConfiguration(it) val newSettings = idpSettings.copy( - endpointToken = config.token_endpoint, - endpointAuthorization = config.authorization_endpoint, - endpointDeviceAuthorization = config.device_authorization_endpoint, - endpointEndSession = config.end_session_endpoint, - endpointIntrospection = config.introspection_endpoint, - endpointUserInfo = config.userinfo_endpoint + endpointToken = config.tokenEndpoint, + endpointAuthorization = config.authorizationEndpoint, + endpointDeviceAuthorization = config.deviceAuthorizationEndpoint, + endpointEndSession = config.endSessionEndpoint, + endpointIntrospection = config.introspectionEndpoint, + endpointUserInfo = config.userinfoEndpoint ) settingsStore.setIdpSettings(newSettings) } @@ -128,4 +143,4 @@ class ConfigPresenter( eventSink = ::eventSink ) } -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigPresenterFactory.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigPresenterFactory.kt index 2f35048d..174d5266 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigPresenterFactory.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigPresenterFactory.kt @@ -8,8 +8,7 @@ import com.slack.circuit.runtime.ui.Ui import com.slack.circuit.runtime.ui.ui import org.publicvalue.multiplatform.oidc.sample.screens.ConfigScreen - -object ConfigPresenterFactory: Presenter.Factory { +internal object ConfigPresenterFactory : Presenter.Factory { override fun create( screen: Screen, navigator: Navigator, @@ -22,7 +21,7 @@ object ConfigPresenterFactory: Presenter.Factory { } } -object ConfigUiFactory: Ui.Factory { +internal object ConfigUiFactory : Ui.Factory { override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) { is ConfigScreen -> { ui { state, modifier -> @@ -31,4 +30,4 @@ object ConfigUiFactory: Ui.Factory { } else -> null } -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigUiState.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigUiState.kt index 84e428ac..230d3beb 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigUiState.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/ConfigUiState.kt @@ -2,30 +2,28 @@ package org.publicvalue.multiplatform.oidc.sample.config import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState -import org.publicvalue.multiplatform.oidc.sample.config.ConfigUiEvent import org.publicvalue.multiplatform.oidc.sample.domain.ClientSettings import org.publicvalue.multiplatform.oidc.sample.domain.IdpSettings import org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod -import kotlin.reflect.KProperty1 -data class ConfigUiState( +internal data class ConfigUiState( val clientSettings: ClientSettings, val idpSettings: IdpSettings, val eventSink: (ConfigUiEvent) -> Unit -): CircuitUiState +) : CircuitUiState -sealed interface ConfigUiEvent: CircuitUiEvent { - data object NavigateBack: ConfigUiEvent +internal sealed interface ConfigUiEvent : CircuitUiEvent { + data object NavigateBack : ConfigUiEvent - data class ChangeDiscoveryUrl(val discoveryUrl: String): ConfigUiEvent - data class ChangeEndpointToken(val endpointToken: String): ConfigUiEvent - data class ChangeEndpointAuthorization(val endpointAuthorization: String): ConfigUiEvent - data class ChangeEndpointEndSession(val endpointEndSession: String): ConfigUiEvent + data class ChangeDiscoveryUrl(val discoveryUrl: String) : ConfigUiEvent + data class ChangeEndpointToken(val endpointToken: String) : ConfigUiEvent + data class ChangeEndpointAuthorization(val endpointAuthorization: String) : ConfigUiEvent + data class ChangeEndpointEndSession(val endpointEndSession: String) : ConfigUiEvent - data class ChangeClientId(val clientId: String): ConfigUiEvent - data class ChangeClientSecret(val clientSecret: String): ConfigUiEvent - data class ChangeScope(val scope: String): ConfigUiEvent - data class ChangeCodeChallengeMethod(val codeChallengeMethod: CodeChallengeMethod): ConfigUiEvent + data class ChangeClientId(val clientId: String) : ConfigUiEvent + data class ChangeClientSecret(val clientSecret: String) : ConfigUiEvent + data class ChangeScope(val scope: String) : ConfigUiEvent + data class ChangeCodeChallengeMethod(val codeChallengeMethod: CodeChallengeMethod) : ConfigUiEvent - data object Discover: ConfigUiEvent -} \ No newline at end of file + data object Discover : ConfigUiEvent +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/FormHeadline.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/FormHeadline.kt index 4c5b1fdf..aa03b358 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/FormHeadline.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/FormHeadline.kt @@ -8,12 +8,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun FormHeadline( - modifier:Modifier = Modifier, +internal fun FormHeadline( + modifier: Modifier = Modifier, text: String ) { Text( modifier = modifier.padding(vertical = 16.dp), - style = typography.titleSmall, text = text + style = typography.titleSmall, + text = text ) -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/SingleLineInput.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/SingleLineInput.kt index 5e428a3e..d985a054 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/SingleLineInput.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/config/SingleLineInput.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @Composable -fun SingleLineInput(value: String, onValueChange: (String) -> Unit, label: @Composable () -> Unit) { +internal fun SingleLineInput(value: String, onValueChange: (String) -> Unit, label: @Composable () -> Unit) { TextField( modifier = Modifier.fillMaxWidth(), singleLine = true, @@ -19,4 +19,3 @@ fun SingleLineInput(value: String, onValueChange: (String) -> Unit, label: @Comp label = label ) } - diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/data/LocalSettingsStore.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/data/LocalSettingsStore.kt index daf736b5..00b8e29e 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/data/LocalSettingsStore.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/data/LocalSettingsStore.kt @@ -2,6 +2,9 @@ package org.publicvalue.multiplatform.oidc.sample.data import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf -import org.publicvalue.multiplatform.oidc.settings.SettingsStore -val LocalSettingsStore: ProvidableCompositionLocal = compositionLocalOf { error("Has to be provided") } \ No newline at end of file +internal val LocalSettingsStore: ProvidableCompositionLocal = compositionLocalOf { + error( + "Has to be provided" + ) +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/data/OidcSettingsStore.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/data/OidcSettingsStore.kt index eaa0c7d6..dd7f9e7d 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/data/OidcSettingsStore.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/data/OidcSettingsStore.kt @@ -13,11 +13,11 @@ import org.publicvalue.multiplatform.oidc.sample.domain.IdpSettings import org.publicvalue.multiplatform.oidc.sample.domain.TokenData import org.publicvalue.multiplatform.oidc.settings.SettingsStore -private val IDP_SETTINGS_KEY = "idp_settings_key" -private val CLIENT_SETTINGS_KEY = "client_settings_key" -private val TOKEN_DATA_KEY = "token_data_key" +private const val IDP_SETTINGS_KEY = "idp_settings_key" +private const val CLIENT_SETTINGS_KEY = "client_settings_key" +private const val TOKEN_DATA_KEY = "token_data_key" -class OidcSettingsStore( +internal class OidcSettingsStore( private val settingsStore: SettingsStore ) { private val idpSettings = MutableStateFlow(null) @@ -29,7 +29,7 @@ class OidcSettingsStore( fun observeTokenData() = tokenData.asStateFlow() private val scope by lazy { CoroutineScope(Dispatchers.Default + SupervisorJob()) } - + init { scope.launch { settingsStore.get(IDP_SETTINGS_KEY)?.let { @@ -60,4 +60,4 @@ class OidcSettingsStore( settingsStore.remove(TOKEN_DATA_KEY) this.tokenData.value = null } -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/ClientSettings.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/ClientSettings.kt index 5051b45f..2760c066 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/ClientSettings.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/ClientSettings.kt @@ -1,15 +1,19 @@ package org.publicvalue.multiplatform.oidc.sample.domain +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.publicvalue.multiplatform.oidc.types.CodeChallengeMethod @Serializable -data class ClientSettings( +internal data class ClientSettings( val name: String? = null, - val client_id: String? = null, - val client_secret: String? = null, + @SerialName("client_id") + val clientId: String? = null, + @SerialName("client_secret") + val clientSecret: String? = null, val scope: String? = null, - val code_challenge_method: CodeChallengeMethod = CodeChallengeMethod.S256, + @SerialName("code_challenge_method") + val codeChallengeMethod: CodeChallengeMethod = CodeChallengeMethod.S256, ) { companion object { val Empty = ClientSettings( @@ -18,6 +22,6 @@ data class ClientSettings( } fun isValid(): Boolean { - return client_id != null && scope != null + return clientId != null && scope != null } -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/IdpSettings.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/IdpSettings.kt index c8e6b75e..3b102528 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/IdpSettings.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/IdpSettings.kt @@ -3,7 +3,7 @@ package org.publicvalue.multiplatform.oidc.sample.domain import kotlinx.serialization.Serializable @Serializable -data class IdpSettings( +internal data class IdpSettings( val discoveryUrl: String? = null, val endpointToken: String? = null, val endpointAuthorization: String? = null, @@ -20,4 +20,3 @@ data class IdpSettings( return endpointToken != null && endpointAuthorization != null } } - diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/TokenData.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/TokenData.kt index 8cf6a971..16a0326b 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/TokenData.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/domain/TokenData.kt @@ -3,10 +3,10 @@ package org.publicvalue.multiplatform.oidc.sample.domain import kotlinx.serialization.Serializable @Serializable -data class TokenData( +internal data class TokenData( val accessToken: String?, val refreshToken: String?, val idToken: String?, val expiresIn: Int, val issuedAt: Long -) \ No newline at end of file +) diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/Home.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/Home.kt index d0aab537..3135ec4b 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/Home.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/Home.kt @@ -29,15 +29,10 @@ import org.publicvalue.multiplatform.oidc.sample.domain.TokenData @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Home( +internal fun Home( state: HomeUiState, modifier: Modifier = Modifier ) { - var greetingText by remember { mutableStateOf("Hello, World!") } - var showImage by remember { mutableStateOf(false) } - -// val settingsStore = LocalSettingsStore.current - Scaffold( topBar = { TopAppBar( @@ -66,7 +61,7 @@ fun Home( } @Composable -fun Home( +internal fun Home( modifier: Modifier = Modifier, loginEnabled: Boolean, refreshEnabled: Boolean, @@ -85,7 +80,6 @@ fun Home( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Row { Text("Token Data:") if (tokenData == null) { @@ -93,24 +87,28 @@ fun Home( } } tokenData?.let { - Row() { + Row { Text("Access token:") Text(it.accessToken ?: "") } } tokenData?.let { - Row() { + Row { Text("Token lifetime:") Text("${it.expiresIn}") } } - tokenData?.let { Row() { - Text("Refresh token:") - Text(it.refreshToken ?: "") } + tokenData?.let { + Row { + Text("Refresh token:") + Text(it.refreshToken ?: "") + } } - subject?.let { Row() { - Text("Subject:") - Text(subject) } + subject?.let { + Row { + Text("Subject:") + Text(subject) + } } errorMessage?.let { Text("Error: $it") @@ -129,17 +127,21 @@ fun Home( Text("Login") } - Button(onClick = { - onLogoutClick(useWebFlow) - }, - enabled = logoutEnabled) { + Button( + onClick = { + onLogoutClick(useWebFlow) + }, + enabled = logoutEnabled + ) { Text("Logout") } - Button(onClick = { - onRefreshClick() - }, - enabled = refreshEnabled) { + Button( + onClick = { + onRefreshClick() + }, + enabled = refreshEnabled + ) { Text("Refresh") } } @@ -149,8 +151,8 @@ fun Home( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Switch(useWebFlow, onCheckedChange = {useWebFlow = it}) + Switch(useWebFlow, onCheckedChange = { useWebFlow = it }) Text("Use GET request with post_endsession_redirect_uri for logout") } } -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomePresenter.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomePresenter.kt index 312ff031..4839cbac 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomePresenter.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomePresenter.kt @@ -1,12 +1,19 @@ package org.publicvalue.multiplatform.oidc.sample.home -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.http.* +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.parameter +import io.ktor.client.request.url +import io.ktor.http.HttpStatusCode +import io.ktor.http.URLBuilder +import io.ktor.http.isSuccess import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import org.publicvalue.multiplatform.oidc.DefaultOpenIdConnectClient.Companion.DefaultHttpClient @@ -20,10 +27,10 @@ import org.publicvalue.multiplatform.oidc.sample.screens.ConfigScreen import org.publicvalue.multiplatform.oidc.types.Jwt import org.publicvalue.multiplatform.oidc.types.remote.AccessTokenResponse -class HomePresenter( +internal class HomePresenter( val authFlowFactory: CodeAuthFlowFactory, val navigator: Navigator -): ErrorPresenter { +) : ErrorPresenter { override var errorMessage = MutableStateFlow(null) @@ -48,10 +55,10 @@ class HomePresenter( OpenIdConnectClient(idpSettings.discoveryUrl) { redirectUri = PlatformConstants.redirectUrl.trim() postLogoutRedirectUri = PlatformConstants.postLogoutRedirectUrl.trim() - codeChallengeMethod = clientSettings.code_challenge_method + codeChallengeMethod = clientSettings.codeChallengeMethod this.scope = clientSettings.scope?.trim() - this.clientId = clientSettings.client_id?.trim() - this.clientSecret = clientSettings.client_secret?.trim() + this.clientId = clientSettings.clientId?.trim() + this.clientSecret = clientSettings.clientSecret?.trim() this.endpoints { authorizationEndpoint = idpSettings.endpointAuthorization?.trim() tokenEndpoint = idpSettings.endpointToken?.trim() @@ -65,22 +72,22 @@ class HomePresenter( suspend fun updateTokenResponse(newTokens: AccessTokenResponse) { tokenResponse = newTokens - val jwt = newTokens.id_token?.let { Jwt.parse(it) } + val jwt = newTokens.idToken?.let { Jwt.parse(it) } println("parsed jwt: $jwt") subject = jwt?.payload?.sub settingsStore.setTokenData( org.publicvalue.multiplatform.oidc.sample.domain.TokenData( - accessToken = newTokens.access_token, - refreshToken = newTokens.refresh_token, - idToken = newTokens.id_token, - expiresIn = newTokens.expires_in ?: 0, - issuedAt = newTokens.received_at + accessToken = newTokens.accessToken, + refreshToken = newTokens.refreshToken, + idToken = newTokens.idToken, + expiresIn = newTokens.expiresIn ?: 0, + issuedAt = newTokens.receivedAt ) ) } fun eventSink(event: HomeUiEvent) { - when(event) { + when (event) { HomeUiEvent.Login -> { resetErrorMessage() val client = createClient() @@ -107,24 +114,26 @@ class HomePresenter( val isGoogle = client.config.discoveryUri.toString().contains("accounts.google.com") if (!client.config.endpoints?.endSessionEndpoint.isNullOrEmpty() || isGoogle) { catchErrorMessage { - val result = if(isGoogle) { + val result = if (isGoogle) { val endpoint = "https://accounts.google.com/o/oauth2/revoke" val url = URLBuilder(endpoint) val response = DefaultHttpClient.submitForm { url(url.build()) - parameter("token", it.access_token) + parameter("token", it.accessToken) } response.status } else { if (event.useWebFlow) { val flow = authFlowFactory.createEndSessionFlow(client) - val result = flow.endSession(it.id_token ?: "") + val result = flow.endSession(it.idToken ?: "") if (result.isFailure) { - setErrorMessage(result.exceptionOrNull()?.message ?: "Unknown error") + setErrorMessage( + result.exceptionOrNull()?.message ?: "Unknown error" + ) } if (result.isSuccess) HttpStatusCode.OK else null } else { - client.endSession(idToken = it.id_token ?: "") + client.endSession(idToken = it.idToken ?: "") } } if (result?.isSuccess() == true || result == HttpStatusCode.Found) { @@ -152,7 +161,7 @@ class HomePresenter( scope.launch { catchErrorMessage { tokenResponse?.let { - val newTokens = client.refreshToken(refreshToken = it.refresh_token ?: "") + val newTokens = client.refreshToken(refreshToken = it.refreshToken ?: "") updateTokenResponse(newTokens) } } @@ -172,4 +181,4 @@ class HomePresenter( errorMessage = errorMessage ) } -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomePresenterFactory.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomePresenterFactory.kt index 30c73314..67e83ad0 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomePresenterFactory.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomePresenterFactory.kt @@ -9,8 +9,7 @@ import com.slack.circuit.runtime.ui.ui import org.publicvalue.multiplatform.oidc.appsupport.CodeAuthFlowFactory import org.publicvalue.multiplatform.oidc.sample.screens.HomeScreen - -class HomePresenterFactory(val authFlowFactory: CodeAuthFlowFactory): Presenter.Factory { +internal class HomePresenterFactory(val authFlowFactory: CodeAuthFlowFactory) : Presenter.Factory { override fun create( screen: Screen, navigator: Navigator, @@ -23,7 +22,7 @@ class HomePresenterFactory(val authFlowFactory: CodeAuthFlowFactory): Presenter. } } -object HomeUiFactory: Ui.Factory { +internal object HomeUiFactory : Ui.Factory { override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) { is HomeScreen -> { ui { state, modifier -> @@ -32,4 +31,4 @@ object HomeUiFactory: Ui.Factory { } else -> null } -} \ No newline at end of file +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomeUiState.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomeUiState.kt index 54524edd..c12a6e3d 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomeUiState.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/home/HomeUiState.kt @@ -4,7 +4,7 @@ import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import org.publicvalue.multiplatform.oidc.sample.domain.TokenData -data class HomeUiState( +internal data class HomeUiState( val loginEnabled: Boolean, val refreshEnabled: Boolean, val logoutEnabled: Boolean, @@ -12,11 +12,11 @@ data class HomeUiState( val subject: String?, val eventSink: (HomeUiEvent) -> Unit, val errorMessage: String? -): CircuitUiState +) : CircuitUiState -sealed interface HomeUiEvent: CircuitUiEvent { - data object NavigateToConfig: HomeUiEvent - data object Login: HomeUiEvent - data class Logout(val useWebFlow: Boolean): HomeUiEvent - data object Refresh: HomeUiEvent -} \ No newline at end of file +internal sealed interface HomeUiEvent : CircuitUiEvent { + data object NavigateToConfig : HomeUiEvent + data object Login : HomeUiEvent + data class Logout(val useWebFlow: Boolean) : HomeUiEvent + data object Refresh : HomeUiEvent +} diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.kt index 1f94c9d6..4f1e1fbc 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.kt @@ -1,5 +1,5 @@ package org.publicvalue.multiplatform.oidc.sample.screens -annotation class CommonParcelize +internal annotation class CommonParcelize -expect interface CommonParcelable +internal expect interface CommonParcelable diff --git a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Screens.kt b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Screens.kt index 58cd836a..bc5bdd1b 100644 --- a/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Screens.kt +++ b/sample-app/shared/src/commonMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Screens.kt @@ -3,10 +3,11 @@ package org.publicvalue.multiplatform.oidc.sample.screens import com.slack.circuit.runtime.screen.Screen @CommonParcelize -object HomeScreen : SampleAppScreen(name = "Home()") +internal object HomeScreen : SampleAppScreen(name = "Home()") + @CommonParcelize -object ConfigScreen : SampleAppScreen(name = "Config()") +internal object ConfigScreen : SampleAppScreen(name = "Config()") -abstract class SampleAppScreen(val name: String) : Screen { +internal abstract class SampleAppScreen(val name: String) : Screen { open val arguments: Map? = null -} \ No newline at end of file +} diff --git a/sample-app/shared/src/iosMain/kotlin/main.ios.kt b/sample-app/shared/src/iosMain/kotlin/main.ios.kt index f9088c17..c1fbd02f 100644 --- a/sample-app/shared/src/iosMain/kotlin/main.ios.kt +++ b/sample-app/shared/src/iosMain/kotlin/main.ios.kt @@ -4,9 +4,10 @@ import com.slack.circuit.foundation.rememberCircuitNavigator import org.publicvalue.multiplatform.oidc.appsupport.IosCodeAuthFlowFactory import org.publicvalue.multiplatform.oidc.sample.screens.HomeScreen import org.publicvalue.multiplatform.oidc.settings.IosSettingsStore +import platform.UIKit.UIViewController -fun MainViewController() = ComposeUIViewController { - +@Suppress("FunctionName") +public fun MainViewController(): UIViewController = ComposeUIViewController { val factory = IosCodeAuthFlowFactory() val backstack = rememberSaveableBackStack(listOf(HomeScreen)) @@ -20,4 +21,4 @@ fun MainViewController() = ComposeUIViewController { settingsStore = settingsStore, authFlowFactory = factory ) -} \ No newline at end of file +} diff --git a/sample-app/shared/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.ios.kt b/sample-app/shared/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.ios.kt index 35c99d5c..d35049a5 100644 --- a/sample-app/shared/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.ios.kt +++ b/sample-app/shared/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.ios.kt @@ -1,6 +1,6 @@ package org.publicvalue.multiplatform.oidc.sample -actual object PlatformConstants : Constants { +internal actual object PlatformConstants : Constants { override val redirectUrl: String = "org.publicvalue.multiplatform.oidc.sample://redirect" override val postLogoutRedirectUrl: String = "org.publicvalue.multiplatform.oidc.sample://logout" -} \ No newline at end of file +} diff --git a/sample-app/shared/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.ios.kt b/sample-app/shared/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.ios.kt index 313852a8..fd1707d2 100644 --- a/sample-app/shared/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.ios.kt +++ b/sample-app/shared/src/iosMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.ios.kt @@ -1,3 +1,3 @@ package org.publicvalue.multiplatform.oidc.sample.screens -actual interface CommonParcelable \ No newline at end of file +public actual interface CommonParcelable diff --git a/sample-app/shared/src/jvmMain/kotlin/main.desktop.kt b/sample-app/shared/src/jvmMain/kotlin/main.desktop.kt index 15144490..c69ed88c 100644 --- a/sample-app/shared/src/jvmMain/kotlin/main.desktop.kt +++ b/sample-app/shared/src/jvmMain/kotlin/main.desktop.kt @@ -1,13 +1,14 @@ import androidx.compose.runtime.Composable import com.slack.circuit.backstack.rememberSaveableBackStack import com.slack.circuit.foundation.rememberCircuitNavigator +import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect import org.publicvalue.multiplatform.oidc.appsupport.JvmCodeAuthFlowFactory import org.publicvalue.multiplatform.oidc.sample.screens.HomeScreen import org.publicvalue.multiplatform.oidc.settings.JvmSettingsStore +@OptIn(ExperimentalOpenIdConnect::class) @Composable -fun MainView() { - +public fun MainView() { val backstack = rememberSaveableBackStack(listOf(HomeScreen)) val navigator = rememberCircuitNavigator(backstack) { } diff --git a/sample-app/shared/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.jvm.kt b/sample-app/shared/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.jvm.kt index 0ed49d79..6f6f858c 100644 --- a/sample-app/shared/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.jvm.kt +++ b/sample-app/shared/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.jvm.kt @@ -1,6 +1,6 @@ package org.publicvalue.multiplatform.oidc.sample -actual object PlatformConstants : Constants { +internal actual object PlatformConstants : Constants { override val redirectUrl: String = "http://localhost:8080/redirect" override val postLogoutRedirectUrl: String = "http://localhost:8080/logout" -} \ No newline at end of file +} diff --git a/sample-app/shared/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.jvm.kt b/sample-app/shared/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.jvm.kt index 313852a8..6712370d 100644 --- a/sample-app/shared/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.jvm.kt +++ b/sample-app/shared/src/jvmMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.jvm.kt @@ -1,3 +1,3 @@ package org.publicvalue.multiplatform.oidc.sample.screens -actual interface CommonParcelable \ No newline at end of file +internal actual interface CommonParcelable diff --git a/sample-app/shared/src/wasmJsMain/kotlin/main.wasmJs.kt b/sample-app/shared/src/wasmJsMain/kotlin/main.wasmJs.kt index 36e9bbb0..e9dfda17 100644 --- a/sample-app/shared/src/wasmJsMain/kotlin/main.wasmJs.kt +++ b/sample-app/shared/src/wasmJsMain/kotlin/main.wasmJs.kt @@ -9,7 +9,7 @@ import org.publicvalue.multiplatform.oidc.settings.WasmJsSettingsStore @OptIn(ExperimentalOpenIdConnect::class) @Composable -fun MainView() { +public fun MainView() { val backstack = rememberSaveableBackStack(listOf(HomeScreen)) val navigator = rememberCircuitNavigator(backstack) {} diff --git a/sample-app/shared/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.wasmJs.kt b/sample-app/shared/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.wasmJs.kt index 0ed49d79..6f6f858c 100644 --- a/sample-app/shared/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.wasmJs.kt +++ b/sample-app/shared/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/sample/Constants.wasmJs.kt @@ -1,6 +1,6 @@ package org.publicvalue.multiplatform.oidc.sample -actual object PlatformConstants : Constants { +internal actual object PlatformConstants : Constants { override val redirectUrl: String = "http://localhost:8080/redirect" override val postLogoutRedirectUrl: String = "http://localhost:8080/logout" -} \ No newline at end of file +} diff --git a/sample-app/shared/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.wasmJs.kt b/sample-app/shared/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.wasmJs.kt index 313852a8..6712370d 100644 --- a/sample-app/shared/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.wasmJs.kt +++ b/sample-app/shared/src/wasmJsMain/kotlin/org/publicvalue/multiplatform/oidc/sample/screens/Parcelize.wasmJs.kt @@ -1,3 +1,3 @@ package org.publicvalue.multiplatform.oidc.sample.screens -actual interface CommonParcelable \ No newline at end of file +internal actual interface CommonParcelable diff --git a/sample-app/wasm-js-app/code-quality/baseline.xml b/sample-app/wasm-js-app/code-quality/baseline.xml new file mode 100644 index 00000000..3447d9c7 --- /dev/null +++ b/sample-app/wasm-js-app/code-quality/baseline.xml @@ -0,0 +1,7 @@ + + + + + Filename:main.kt$.main.kt + + diff --git a/sample-app/wasm-js-app/src/wasmJsMain/kotlin/main.kt b/sample-app/wasm-js-app/src/wasmJsMain/kotlin/main.kt index 3873097b..d8753c56 100644 --- a/sample-app/wasm-js-app/src/wasmJsMain/kotlin/main.kt +++ b/sample-app/wasm-js-app/src/wasmJsMain/kotlin/main.kt @@ -6,7 +6,7 @@ import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect import org.publicvalue.multiplatform.oidc.appsupport.PlatformCodeAuthFlow @OptIn(ExperimentalComposeUiApi::class, ExperimentalOpenIdConnect::class) -fun main() { +internal fun main() { @Suppress("DEPRECATION_ERROR") CanvasBasedWindow(canvasElementId = "wasm-js-app") { val currentPath = window.location.pathname @@ -21,4 +21,4 @@ fun main() { } } } -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 183a2ca8..156255a5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,7 +10,9 @@ pluginManagement { } dependencyResolutionManagement { + @Suppress("UnstableApiUsage") repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + @Suppress("UnstableApiUsage") repositories { mavenCentral() google() @@ -50,15 +52,37 @@ dependencyResolutionManagement { } } -rootProject.name="kotlin-multiplatform-oidc" +rootProject.name = "kotlin-multiplatform-oidc" -include(":oidc-crypto") -include(":oidc-core") -include(":oidc-appsupport") -include(":oidc-tokenstore") -include(":oidc-okhttp4") -include(":oidc-ktor") +include( + ":oidc-crypto", + ":oidc-core", + ":oidc-appsupport", + ":oidc-tokenstore", + ":oidc-okhttp4", + ":oidc-ktor" +) + +// uncomment this line, if you want to testing +// includeBuild("sample-app") +// includeBuild("playground-app") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") // https://docs.gradle.org/8.3/userguide/configuration_cache.html#config_cache:stable -// enableFeaturePreview("STABLE_CONFIGURATION_CACHE") \ No newline at end of file +// enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +gradle.projectsLoaded { + + if (System.getenv("CI") == "true") { + return@projectsLoaded + } + + val hookFile = File(rootDir, ".git/hooks/pre-push") + if (!hookFile.exists()) { + println("🪝 Installing pre-push hook...") + val prePushTasks = File(rootDir, "build-logic/scripts/pre-push") + prePushTasks.copyTo(hookFile, overwrite = true) + hookFile.setExecutable(true) + println("✅ Pre-push hook installed") + } +}