diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6f72ffa --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +# JIRA Ticket + +[JIRA](https://bugster.forgerock.org/jira/browse/SDKS-xxx) "Jira ticket title..." + +# Description + +Briefly describe the change and any information that would help speedup the review and testing process. + +# Checklist: + +- [ ] I ran all unit tests and they pass +- [ ] I added test case coverage for my changes diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml new file mode 100644 index 0000000..e40e208 --- /dev/null +++ b/.github/workflows/build-and-test.yaml @@ -0,0 +1,58 @@ +name: Build and Test + +on: + workflow_call: + secrets: + SLACK_WEBHOOK: + description: 'Slack Notifier Webhook' + required: true +jobs: + build-and-test: + strategy: + fail-fast: false + matrix: + os: [macos-14, macos-14-large] + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + + steps: + # Clone the repo + - name: Clone the repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{github.event.pull_request.head.repo.full_name}} + fetch-depth: 0 + + # Get the architecture of the chip + - name: Check chip architecture + run: echo "CHIP_TYPE=$(uname -m)" >> $GITHUB_ENV + + # Set target Xcode version. For more details and options see: + # https://github.com/actions/virtual-environments/blob/main/images/macos/macos-14-Readme.md + - name: Select Xcode + run: sudo xcode-select -switch /Applications/Xcode_15.4.app && /usr/bin/xcodebuild -version + + # Run all tests + - name: Run tests + run: xcodebuild test -scheme PingTestHost -workspace SampleApps/Ping.xcworkspace -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.5' -derivedDataPath DerivedData -enableCodeCoverage YES -resultBundlePath TestResults | xcpretty && exit ${PIPESTATUS[0]} + + # Publish test results + - name: Publish test results + uses: kishikawakatsumi/xcresulttool@v1 + with: + title: "Test Results ${{ matrix.os }} - ${{ env.CHIP_TYPE }}" + path: TestResults.xcresult + show-passed-tests: false + if: success() || failure() + + # Send slack notification with result status + - uses: 8398a7/action-slack@v3 + with: + mention: 'stoyan.petrov' + if_mention: 'failure,cancelled' + fields: repo,author,eventName,message,job,pullRequest,took + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + if: always() \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..192f6a1 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,27 @@ +name: CI + +# Trigger on push or pull request +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: + - master + - develop + +jobs: + # Build and run unit tests + build-and-test: + name: Build and test + uses: ./.github/workflows/build-and-test.yaml + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + # Run Mend CLI Scan + mend-cli-scan: + name: Mend CLI Scan + uses: ./.github/workflows/mend-cli-scan.yaml + secrets: + MEND_EMAIL: ${{ secrets.MEND_EMAIL }} + MEND_USER_KEY: ${{ secrets.MEND_USER_KEY }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/mend-cli-scan.yaml b/.github/workflows/mend-cli-scan.yaml new file mode 100644 index 0000000..a37551a --- /dev/null +++ b/.github/workflows/mend-cli-scan.yaml @@ -0,0 +1,104 @@ +name: Run Mend CLS Scan +on: + workflow_call: + secrets: + MEND_EMAIL: + description: Mend email + required: true + MEND_USER_KEY: + description: Mend user key + required: true + SLACK_WEBHOOK: + description: Slack Notifier Incoming Webhook + required: true + +jobs: + mend-cli-scan: + runs-on: macos-14 + + steps: + # Clone the repo + - name: Clone the repository + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{github.event.pull_request.head.repo.full_name}} + fetch-depth: 0 + + # Set target Xcode version. For more details and options see: + # https://github.com/actions/virtual-environments/blob/main/images/macos/macos-12-Readme.md + - name: Select Xcode + run: sudo xcode-select -switch /Applications/Xcode_15.4.app && /usr/bin/xcodebuild -version + + # Setup Mend CLI + - name: Download and cache the Mend CLI executable + id: cache-mend + uses: actions/cache@v3 + env: + mend-cache-name: cache-mend-executable + with: + path: /usr/local/bin/mend + key: ${{ runner.os }}-${{ env.mend-cache-name }}-${{ hashFiles('/usr/local/bin/mend') }} + restore-keys: | + ${{ runner.os }}-${{ env.mend-cache-name }}- + + # Download Mend CLI if it's not cached... + - if: ${{ steps.cache-mend.outputs.cache-hit != 'true' }} + name: Download Mend CLI executable (cache miss...) + continue-on-error: true + run: | + echo "Download Mend CLI executable (cache miss...)" + curl https://downloads.mend.io/cli/darwin_amd64/mend -o /usr/local/bin/mend && chmod +x /usr/local/bin/mend + + # Execute the Mend CLI scan + - name: Mend CLI Scan + env: + MEND_EMAIL: ${{secrets.MEND_EMAIL}} + MEND_USER_KEY: ${{secrets.MEND_USER_KEY}} + MEND_URL: ${{ vars.MEND_SERVER_URL }} + run: | + mend dep --no-color -s ${{ vars.MEND_PRODUCT_NAME }}//${{ vars.MEND_PROJECT_NAME }} -u > mend-scan-result.txt + echo "MEND_SCAN_URL=$(cat mend-scan-result.txt | grep -Eo '(http|https)://[a-zA-Z0-9./?!=_%:-\#]*')" >> $GITHUB_ENV + echo "MEND_SCAN_SUMMARY=$(cat mend-scan-result.txt | grep -Eoiw '(Detected [0-9]* vulnerabilities.*)')" >> $GITHUB_ENV + echo "MEND_CRITICAL_COUNT=$(cat mend-scan-result.txt | grep -Eoiw '(Detected [0-9]* vulnerabilities.*)' | grep -oi '[0-9]* Critical' | grep -o [0-9]*)" >> $GITHUB_ENV + echo "MEND_HIGH_COUNT=$(cat mend-scan-result.txt | grep -Eoiw '(Detected [0-9]* vulnerabilities.*)' | grep -oi '[0-9]* High' | grep -o [0-9]*)" >> $GITHUB_ENV + + # Check for failures and set the outcome of the workflow + - name: Parse the result and set job status + if: always() + run: | + if [ '${{ env.MEND_CRITICAL_COUNT }}' -gt '0' ] || [ '${{ env.MEND_HIGH_COUNT }}' -gt '0' ]; then + exit 1 + else + exit 0 + fi + + # Publish the result + - name: Mend Scan Result + uses: LouisBrunner/checks-action@v1.6.1 + if: always() + with: + name: "Mend Scan Result" + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + output_text_description_file: mend-scan-result.txt + output: | + {"title":"Mend Scan Result", "summary":"${{ job.status }}"} + + # Send slack notification with result status + - name: Send slack notification + uses: 8398a7/action-slack@v3 + with: + status: custom + fields: all + custom_payload: | + { + attachments: [{ + title: 'ForgeRock iOS SDK Mend Scan', + color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', + text: `\nStatus: ${{ job.status }}\nWorkflow: ${process.env.AS_WORKFLOW} -> ${process.env.AS_JOB}\nSummary: ${{ env.MEND_SCAN_SUMMARY }}\nScan URL: ${{ env.MEND_SCAN_URL }}`, + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + if: always() \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a60fa2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Third-party dependency +FRGoogleSignIn/FRGoogleSignIn/framework/* + +## Build generated +build/ +DerivedData/ +docs/* +SDKs/* + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint +*.xcworkspacedata +xcuserdata/ + + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3119f16 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +## [1.0.0] +- General Availability release of the Ping SDK for iOS + +#### Added +- Added Logger initial version +- Added Storage initial version +- Added Oidc initial version +- Added Orchestrate initial version +- Added Davinci initial version diff --git a/Davinci/Davinci.xcodeproj/project.pbxproj b/Davinci/Davinci.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bfa550f --- /dev/null +++ b/Davinci/Davinci.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 63; + objects = { + +/* Begin PBXBuildFile section */ + 3A203D7F2BDB136C0020C995 /* DaVinci.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D7E2BDB136C0020C995 /* DaVinci.swift */; }; + 3A203D972BDB2CD70020C995 /* Oidc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D962BDB2CD70020C995 /* Oidc.swift */; }; + 3A203D9B2BDE155D0020C995 /* Transform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D9A2BDE155D0020C995 /* Transform.swift */; }; + 3A292C822BF58462006977EF /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A292C812BF58462006977EF /* Agent.swift */; }; + 3A5441AA2BCDF20700385131 /* PingDavinci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5441A12BCDF20700385131 /* PingDavinci.framework */; }; + 3A5441AF2BCDF20700385131 /* DaVinciTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441AE2BCDF20700385131 /* DaVinciTests.swift */; }; + 3A5441B02BCDF20700385131 /* Davinci.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A5441A42BCDF20700385131 /* Davinci.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3A57CB622C44D68C001EC9A0 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57CB612C44D688001EC9A0 /* User.swift */; }; + 3AC13E332C3FFF1000DEF23A /* CollectorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC13E2F2C3FFF1000DEF23A /* CollectorFactory.swift */; }; + 3AC13E342C3FFF1000DEF23A /* Collector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC13E302C3FFF1000DEF23A /* Collector.swift */; }; + 3AC13E352C3FFF1000DEF23A /* Form.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC13E322C3FFF1000DEF23A /* Form.swift */; }; + 3AC13E362C3FFF1000DEF23A /* Connector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC13E312C3FFF1000DEF23A /* Connector.swift */; }; + A50981C22CEBDF2600F4B487 /* PingOidc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A50981C12CEBDF2600F4B487 /* PingOidc.framework */; }; + A50981C32CEBDF2600F4B487 /* PingOidc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A50981C12CEBDF2600F4B487 /* PingOidc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A51D4CD12C585B4500FE09E0 /* FieldCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD02C585B4500FE09E0 /* FieldCollector.swift */; }; + A51D4CD32C585D7C00FE09E0 /* FlowCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD22C585D7C00FE09E0 /* FlowCollector.swift */; }; + A51D4CD52C585DCD00FE09E0 /* PasswordCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD42C585DCD00FE09E0 /* PasswordCollector.swift */; }; + A51D4CD72C585E2D00FE09E0 /* SubmitCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD62C585E2D00FE09E0 /* SubmitCollector.swift */; }; + A51D4CD92C585E5700FE09E0 /* TextCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD82C585E5700FE09E0 /* TextCollector.swift */; }; + A51D4CDF2C599BFF00FE09E0 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CDE2C599BFF00FE09E0 /* Request.swift */; }; + A51D4CE52C656A0C00FE09E0 /* CollectorRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CE42C656A0C00FE09E0 /* CollectorRegistryTests.swift */; }; + A51D4CEE2C656EE100FE09E0 /* FieldCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CED2C656EE100FE09E0 /* FieldCollectorTests.swift */; }; + A51D4CF02C656FC600FE09E0 /* MockResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CEF2C656FC600FE09E0 /* MockResponse.swift */; }; + A51D4CF22C65729A00FE09E0 /* DaVinciErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF12C65729A00FE09E0 /* DaVinciErrorTests.swift */; }; + A51D4CF42C6572E600FE09E0 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF32C6572E600FE09E0 /* MockURLProtocol.swift */; }; + A51D4CF62C65743100FE09E0 /* MockAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF52C65743100FE09E0 /* MockAPIEndpoint.swift */; }; + A51D4CF82C65978300FE09E0 /* DaVinciIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF72C65978300FE09E0 /* DaVinciIntegrationTests.swift */; }; + A51D4CFA2C6BA75000FE09E0 /* CallbackFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF92C6BA75000FE09E0 /* CallbackFactoryTests.swift */; }; + A5A712482CAC527200B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A712472CAC527200B7DD58 /* PrivacyInfo.xcprivacy */; }; + A5CCE5E72C85E96B00349A5E /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CCE5E62C85E96B00349A5E /* Constants.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3A5441AB2BCDF20700385131 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A5441982BCDF20700385131 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A5441A02BCDF20700385131; + remoteInfo = PingDavinci; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A50981C42CEBDF2600F4B487 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + A50981C32CEBDF2600F4B487 /* PingOidc.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3A203D7E2BDB136C0020C995 /* DaVinci.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaVinci.swift; sourceTree = ""; }; + 3A203D962BDB2CD70020C995 /* Oidc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Oidc.swift; sourceTree = ""; }; + 3A203D9A2BDE155D0020C995 /* Transform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transform.swift; sourceTree = ""; }; + 3A292C812BF58462006977EF /* Agent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Agent.swift; sourceTree = ""; }; + 3A5441A12BCDF20700385131 /* PingDavinci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingDavinci.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A5441A42BCDF20700385131 /* Davinci.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Davinci.h; sourceTree = ""; }; + 3A5441A92BCDF20700385131 /* DavinciTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DavinciTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A5441AE2BCDF20700385131 /* DaVinciTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaVinciTests.swift; sourceTree = ""; }; + 3A57CB612C44D688001EC9A0 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 3AC13E2F2C3FFF1000DEF23A /* CollectorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectorFactory.swift; sourceTree = ""; }; + 3AC13E302C3FFF1000DEF23A /* Collector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collector.swift; sourceTree = ""; }; + 3AC13E312C3FFF1000DEF23A /* Connector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connector.swift; sourceTree = ""; }; + 3AC13E322C3FFF1000DEF23A /* Form.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Form.swift; sourceTree = ""; }; + A50981C12CEBDF2600F4B487 /* PingOidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOidc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A51D4CD02C585B4500FE09E0 /* FieldCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCollector.swift; sourceTree = ""; }; + A51D4CD22C585D7C00FE09E0 /* FlowCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCollector.swift; sourceTree = ""; }; + A51D4CD42C585DCD00FE09E0 /* PasswordCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordCollector.swift; sourceTree = ""; }; + A51D4CD62C585E2D00FE09E0 /* SubmitCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmitCollector.swift; sourceTree = ""; }; + A51D4CD82C585E5700FE09E0 /* TextCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCollector.swift; sourceTree = ""; }; + A51D4CDE2C599BFF00FE09E0 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + A51D4CE42C656A0C00FE09E0 /* CollectorRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectorRegistryTests.swift; sourceTree = ""; }; + A51D4CED2C656EE100FE09E0 /* FieldCollectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCollectorTests.swift; sourceTree = ""; }; + A51D4CEF2C656FC600FE09E0 /* MockResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockResponse.swift; sourceTree = ""; }; + A51D4CF12C65729A00FE09E0 /* DaVinciErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaVinciErrorTests.swift; sourceTree = ""; }; + A51D4CF32C6572E600FE09E0 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; + A51D4CF52C65743100FE09E0 /* MockAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIEndpoint.swift; sourceTree = ""; }; + A51D4CF72C65978300FE09E0 /* DaVinciIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaVinciIntegrationTests.swift; sourceTree = ""; }; + A51D4CF92C6BA75000FE09E0 /* CallbackFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallbackFactoryTests.swift; sourceTree = ""; }; + A5A712472CAC527200B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + A5CCE5E62C85E96B00349A5E /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3A54419E2BCDF20700385131 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A50981C22CEBDF2600F4B487 /* PingOidc.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A5441A62BCDF20700385131 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A5441AA2BCDF20700385131 /* PingDavinci.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3A203D802BDB13FF0020C995 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A50981C12CEBDF2600F4B487 /* PingOidc.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3A5441972BCDF20700385131 = { + isa = PBXGroup; + children = ( + 3A5441A32BCDF20700385131 /* Davinci */, + 3A5441AD2BCDF20700385131 /* DavinciTests */, + 3A5441A22BCDF20700385131 /* Products */, + 3A203D802BDB13FF0020C995 /* Frameworks */, + ); + sourceTree = ""; + }; + 3A5441A22BCDF20700385131 /* Products */ = { + isa = PBXGroup; + children = ( + 3A5441A12BCDF20700385131 /* PingDavinci.framework */, + 3A5441A92BCDF20700385131 /* DavinciTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 3A5441A32BCDF20700385131 /* Davinci */ = { + isa = PBXGroup; + children = ( + A5A712472CAC527200B7DD58 /* PrivacyInfo.xcprivacy */, + 3A5441A42BCDF20700385131 /* Davinci.h */, + A5CCE5E62C85E96B00349A5E /* Constants.swift */, + A51D4CE02C62C4BF00FE09E0 /* collector */, + A51D4CE12C62C53400FE09E0 /* module */, + 3A292C812BF58462006977EF /* Agent.swift */, + 3A203D7E2BDB136C0020C995 /* DaVinci.swift */, + 3A57CB612C44D688001EC9A0 /* User.swift */, + ); + path = Davinci; + sourceTree = ""; + }; + 3A5441AD2BCDF20700385131 /* DavinciTests */ = { + isa = PBXGroup; + children = ( + A509D1CE2C6D09C2003A0006 /* integration tests */, + A509D1CD2C6D06CA003A0006 /* mock */, + A51D4CF92C6BA75000FE09E0 /* CallbackFactoryTests.swift */, + A51D4CE42C656A0C00FE09E0 /* CollectorRegistryTests.swift */, + A51D4CF12C65729A00FE09E0 /* DaVinciErrorTests.swift */, + 3A5441AE2BCDF20700385131 /* DaVinciTests.swift */, + A51D4CED2C656EE100FE09E0 /* FieldCollectorTests.swift */, + ); + path = DavinciTests; + sourceTree = ""; + }; + A509D1CD2C6D06CA003A0006 /* mock */ = { + isa = PBXGroup; + children = ( + A51D4CEF2C656FC600FE09E0 /* MockResponse.swift */, + A51D4CF32C6572E600FE09E0 /* MockURLProtocol.swift */, + A51D4CF52C65743100FE09E0 /* MockAPIEndpoint.swift */, + ); + path = mock; + sourceTree = ""; + }; + A509D1CE2C6D09C2003A0006 /* integration tests */ = { + isa = PBXGroup; + children = ( + A51D4CF72C65978300FE09E0 /* DaVinciIntegrationTests.swift */, + ); + path = "integration tests"; + sourceTree = ""; + }; + A51D4CE02C62C4BF00FE09E0 /* collector */ = { + isa = PBXGroup; + children = ( + 3AC13E302C3FFF1000DEF23A /* Collector.swift */, + 3AC13E2F2C3FFF1000DEF23A /* CollectorFactory.swift */, + A51D4CD02C585B4500FE09E0 /* FieldCollector.swift */, + A51D4CD22C585D7C00FE09E0 /* FlowCollector.swift */, + 3AC13E322C3FFF1000DEF23A /* Form.swift */, + A51D4CD42C585DCD00FE09E0 /* PasswordCollector.swift */, + A51D4CD62C585E2D00FE09E0 /* SubmitCollector.swift */, + A51D4CD82C585E5700FE09E0 /* TextCollector.swift */, + ); + path = collector; + sourceTree = ""; + }; + A51D4CE12C62C53400FE09E0 /* module */ = { + isa = PBXGroup; + children = ( + 3AC13E312C3FFF1000DEF23A /* Connector.swift */, + 3A203D962BDB2CD70020C995 /* Oidc.swift */, + A51D4CDE2C599BFF00FE09E0 /* Request.swift */, + 3A203D9A2BDE155D0020C995 /* Transform.swift */, + ); + path = module; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3A54419C2BCDF20700385131 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A5441B02BCDF20700385131 /* Davinci.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3A5441A02BCDF20700385131 /* PingDavinci */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A5441B32BCDF20700385131 /* Build configuration list for PBXNativeTarget "PingDavinci" */; + buildPhases = ( + 3A54419C2BCDF20700385131 /* Headers */, + 3A54419D2BCDF20700385131 /* Sources */, + 3A54419E2BCDF20700385131 /* Frameworks */, + 3A54419F2BCDF20700385131 /* Resources */, + A50981C42CEBDF2600F4B487 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PingDavinci; + productName = PingDavinci; + productReference = 3A5441A12BCDF20700385131 /* PingDavinci.framework */; + productType = "com.apple.product-type.framework"; + }; + 3A5441A82BCDF20700385131 /* DavinciTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A5441B62BCDF20700385131 /* Build configuration list for PBXNativeTarget "DavinciTests" */; + buildPhases = ( + 3A5441A52BCDF20700385131 /* Sources */, + 3A5441A62BCDF20700385131 /* Frameworks */, + 3A5441A72BCDF20700385131 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3A5441AC2BCDF20700385131 /* PBXTargetDependency */, + ); + name = DavinciTests; + productName = PingDavinciTests; + productReference = 3A5441A92BCDF20700385131 /* DavinciTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3A5441982BCDF20700385131 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1540; + TargetAttributes = { + 3A5441A02BCDF20700385131 = { + CreatedOnToolsVersion = 15.0; + LastSwiftMigration = 1500; + }; + 3A5441A82BCDF20700385131 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = 3A54419B2BCDF20700385131 /* Build configuration list for PBXProject "Davinci" */; + compatibilityVersion = "Xcode 15.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3A5441972BCDF20700385131; + productRefGroup = 3A5441A22BCDF20700385131 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3A5441A02BCDF20700385131 /* PingDavinci */, + 3A5441A82BCDF20700385131 /* DavinciTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3A54419F2BCDF20700385131 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A712482CAC527200B7DD58 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A5441A72BCDF20700385131 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3A54419D2BCDF20700385131 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A51D4CD12C585B4500FE09E0 /* FieldCollector.swift in Sources */, + A51D4CDF2C599BFF00FE09E0 /* Request.swift in Sources */, + A51D4CD32C585D7C00FE09E0 /* FlowCollector.swift in Sources */, + A51D4CD72C585E2D00FE09E0 /* SubmitCollector.swift in Sources */, + 3A292C822BF58462006977EF /* Agent.swift in Sources */, + 3A203D972BDB2CD70020C995 /* Oidc.swift in Sources */, + 3A203D9B2BDE155D0020C995 /* Transform.swift in Sources */, + A5CCE5E72C85E96B00349A5E /* Constants.swift in Sources */, + 3AC13E332C3FFF1000DEF23A /* CollectorFactory.swift in Sources */, + 3AC13E342C3FFF1000DEF23A /* Collector.swift in Sources */, + A51D4CD52C585DCD00FE09E0 /* PasswordCollector.swift in Sources */, + 3AC13E352C3FFF1000DEF23A /* Form.swift in Sources */, + 3AC13E362C3FFF1000DEF23A /* Connector.swift in Sources */, + A51D4CD92C585E5700FE09E0 /* TextCollector.swift in Sources */, + 3A57CB622C44D68C001EC9A0 /* User.swift in Sources */, + 3A203D7F2BDB136C0020C995 /* DaVinci.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A5441A52BCDF20700385131 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A51D4CEE2C656EE100FE09E0 /* FieldCollectorTests.swift in Sources */, + A51D4CFA2C6BA75000FE09E0 /* CallbackFactoryTests.swift in Sources */, + A51D4CF62C65743100FE09E0 /* MockAPIEndpoint.swift in Sources */, + A51D4CE52C656A0C00FE09E0 /* CollectorRegistryTests.swift in Sources */, + A51D4CF02C656FC600FE09E0 /* MockResponse.swift in Sources */, + A51D4CF42C6572E600FE09E0 /* MockURLProtocol.swift in Sources */, + A51D4CF82C65978300FE09E0 /* DaVinciIntegrationTests.swift in Sources */, + 3A5441AF2BCDF20700385131 /* DaVinciTests.swift in Sources */, + A51D4CF22C65729A00FE09E0 /* DaVinciErrorTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3A5441AC2BCDF20700385131 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A5441A02BCDF20700385131 /* PingDavinci */; + targetProxy = 3A5441AB2BCDF20700385131 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 3A5441B12BCDF20700385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 3A5441B22BCDF20700385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 3A5441B42BCDF20700385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingDavinci; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3A5441B52BCDF20700385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingDavinci; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3A5441B72BCDF20700385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingDavinciTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Debug; + }; + 3A5441B82BCDF20700385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingDavinciTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3A54419B2BCDF20700385131 /* Build configuration list for PBXProject "Davinci" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A5441B12BCDF20700385131 /* Debug */, + 3A5441B22BCDF20700385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A5441B32BCDF20700385131 /* Build configuration list for PBXNativeTarget "PingDavinci" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A5441B42BCDF20700385131 /* Debug */, + 3A5441B52BCDF20700385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A5441B62BCDF20700385131 /* Build configuration list for PBXNativeTarget "DavinciTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A5441B72BCDF20700385131 /* Debug */, + 3A5441B82BCDF20700385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3A5441982BCDF20700385131 /* Project object */; +} diff --git a/Davinci/Davinci.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Davinci/Davinci.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Davinci/Davinci.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Davinci/Davinci.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Davinci/Davinci.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..16cf018 --- /dev/null +++ b/Davinci/Davinci.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,17 @@ + + + + + FILEHEADER + +// ___FILENAME___ +// ___PACKAGENAME___ +// +// Copyright (c) ___YEAR___ Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + + diff --git a/Davinci/Davinci.xcodeproj/xcshareddata/xcschemes/Davinci.xcscheme b/Davinci/Davinci.xcodeproj/xcshareddata/xcschemes/Davinci.xcscheme new file mode 100644 index 0000000..9f56ae2 --- /dev/null +++ b/Davinci/Davinci.xcodeproj/xcshareddata/xcschemes/Davinci.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Davinci/Davinci/Agent.swift b/Davinci/Davinci/Agent.swift new file mode 100644 index 0000000..d48f8cf --- /dev/null +++ b/Davinci/Davinci/Agent.swift @@ -0,0 +1,59 @@ +// +// Agent.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingOidc +import PingOrchestrate + +internal class CreateAgent: Agent { + typealias T = Void + + let session: Session + let pkce: Pkce? + var used = false + + init(session: Session, pkce: Pkce?) { + self.session = session + self.pkce = pkce + } + + func config() -> () -> T { + return {} + } + + func endSession(oidcConfig: OidcConfig, idToken: String) async throws -> Bool { + // Since we don't have the Session token, let DaVinci handle the sign-off + return true + } + + func authorize(oidcConfig: OidcConfig) async throws -> AuthCode { + // We don't get the state; The state may not be returned since this is primarily for + // CSRF in redirect-based interactions, and pi.flow doesn't use redirect. + guard !session.value.isEmpty else { + throw OidcError.authorizeError(message: "Please start DaVinci flow to authenticate.") + } + guard !used else { + throw OidcError.authorizeError(message: "Auth code already used, please start DaVinci flow again.") + } + + used = true + return session.authCode(pkce: pkce) + } + +} + + +extension Session { + func authCode(pkce: Pkce?) -> AuthCode { + // parse the response and return the auth code + return AuthCode(code: value, codeVerifier: pkce?.codeVerifier) + } +} diff --git a/Davinci/Davinci/Constants.swift b/Davinci/Davinci/Constants.swift new file mode 100644 index 0000000..dbf0e68 --- /dev/null +++ b/Davinci/Davinci/Constants.swift @@ -0,0 +1,50 @@ +// +// Constants.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +enum Constants { + static let actionKey = "actionKey" + static let formData = "formData" + static let type = "type" + static let key = "key" + static let label = "label" + static let form = "form" + static let components = "components" + static let fields = "fields" + static let eventType = "eventType" + static let name = "name" + static let id = "id" + static let data = "data" + static let eventName = "eventName" + static let parameters = "parameters" + static let description = "description" + static let category = "category" + static let next = "next" + static let href = "href" + static let _links = "_links" + static let message = "message" + static let status = "status" + static let authorizeResponse = "authorizeResponse" + static let code = "code" + static let code_1999 = 1999 + static let FAILED = "FAILED" + static let TEXT = "TEXT" + static let PASSWORD = "PASSWORD" + static let SUBMIT_BUTTON = "SUBMIT_BUTTON" + static let FLOW_BUTTON = "FLOW_BUTTON" + static let error = "error" + static let connectorId = "connectorId" + static let capabilityName = "capabilityName" + static let requestTimedOut = "requestTimedOut" + static let pingOneAuthenticationConnector = "pingOneAuthenticationConnector" + static let returnSuccessResponseRedirect = "returnSuccessResponseRedirect" + static let setSession = "setSession" + static let location = "Location" +} diff --git a/Davinci/Davinci/Davinci.h b/Davinci/Davinci/Davinci.h new file mode 100644 index 0000000..2f3cb1a --- /dev/null +++ b/Davinci/Davinci/Davinci.h @@ -0,0 +1,21 @@ +// +// Davinci.h +// Davinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +#import + +//! Project version number for Davinci. +FOUNDATION_EXPORT double DavinciVersionNumber; + +//! Project version string for Davinci. +FOUNDATION_EXPORT const unsigned char DavinciVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Davinci/Davinci/Davinci.swift b/Davinci/Davinci/Davinci.swift new file mode 100644 index 0000000..db5a303 --- /dev/null +++ b/Davinci/Davinci/Davinci.swift @@ -0,0 +1,40 @@ +// +// DaVinci.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingOrchestrate +import PingOidc + +public typealias DaVinci = Workflow +public typealias DaVinciConfig = WorkflowConfig + +extension DaVinci { + /// Method to create a DaVinci instance. + /// - Parameter block: The configuration block. + /// - Returns: The DaVinci instance. + public static func createDaVinci(block: (DaVinciConfig) -> Void = {_ in }) -> DaVinci { + let config = DaVinciConfig() + config.module(CustomHeader.config) { customHeaderConfig in + customHeaderConfig.header(name: Request.Constants.xRequestedWith, value: Request.Constants.pingSdk) + customHeaderConfig.header(name: Request.Constants.xRequestedPlatform, value: Request.Constants.ios) + } + config.module(NodeTransformModule.config) + config.module(OidcModule.config) + config.module(CookieModule.config) { cookieConfig in + cookieConfig.persist = [Request.Constants.stCookie, Request.Constants.stNoSsCookie] + } + + // Apply custom configuration + block(config) + + return DaVinci(config: config) + } +} diff --git a/Davinci/Davinci/PrivacyInfo.xcprivacy b/Davinci/Davinci/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..fcfc9b9 --- /dev/null +++ b/Davinci/Davinci/PrivacyInfo.xcprivacy @@ -0,0 +1,10 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/Davinci/Davinci/User.swift b/Davinci/Davinci/User.swift new file mode 100644 index 0000000..52f6c5e --- /dev/null +++ b/Davinci/Davinci/User.swift @@ -0,0 +1,112 @@ +// +// User.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import PingOidc +import PingOrchestrate + +extension DaVinci { + /// Retrieve the user. + /// If cookies are available, it prepares a new user and returns it. + /// If no user is found and no cookies are available, it returns nil. + /// - Returns: The user if found, otherwise nil. + public func user() async -> User? { + try? await initialize() + + if let cachedUser = self.sharedContext.get(key: SharedContext.Keys.userKey) as? User { + return cachedUser + } + + if await hasCookies() { + if let oidcClientConfig = self.sharedContext.get(key: SharedContext.Keys.oidcClientConfigKey) as? OidcClientConfig { + return await prepareUser(daVinci: self, user: OidcUser(config: oidcClientConfig)) + } + } + return nil + } + + /// Alias for the DaVinci.user() method. + /// - Returns: The user if found, otherwise nil. + public func daVinciUser() async -> User? { + return await user() + } + + /// Method to prepare the user. + /// This Method creates a new UserDelegate instance and caches it in the context. + /// - Parameters: + /// - daVinci: The DaVinci instance. + /// - user: The user. + /// - session: The session. + /// - Returns: The prepared user. + func prepareUser( + daVinci: DaVinci, + user: User, + session: Session = EmptySession() + ) async -> UserDelegate { + let userDelegate = UserDelegate(daVinci: daVinci, user: user, session: session) + // Cache the user in the context + self.sharedContext.set(key: SharedContext.Keys.userKey, value: userDelegate) + return userDelegate + } +} + + +extension SuccessNode { + /// Extension property for SuccessNode to cast the `SuccessNode.session` to a User. + public var user: User? { + return session as? User + } +} + + +/// Struct representing a UserDelegate. +/// This struct is a delegate for the User and Session interfaces. +/// It overrides the logout function to remove the cached user from the context and sign off the user. +/// - property daVinci: The DaVinci instance. +/// - property user: The user. +/// - property session: The session. +struct UserDelegate: User, Session { + private let daVinci: DaVinci + private let user: User + private let session: Session + + init(daVinci: DaVinci, user: User, session: Session) { + self.daVinci = daVinci + self.user = user + self.session = session + } + + /// Method to log out the user. + /// This method removes the cached user from the context and signs off the user. + func logout() async { + // remove the cached user from the context + _ = daVinci.sharedContext.removeValue(forKey: SharedContext.Keys.userKey) + // instead of calling `OidcClient.endSession` directly, we call `DaVinci.signOff` to sign off the user + _ = await daVinci.signOff() + } + + func token() async -> Result { + return await user.token() + } + + func revoke() async { + await user.revoke() + } + + func userinfo(cache: Bool) async -> Result { + await user.userinfo(cache: cache) + } + + var value: String { + get { + return session.value + } + } +} diff --git a/Davinci/Davinci/collector/Collector.swift b/Davinci/Davinci/collector/Collector.swift new file mode 100644 index 0000000..2ec3b6f --- /dev/null +++ b/Davinci/Davinci/collector/Collector.swift @@ -0,0 +1,77 @@ +// +// FlowCollector.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + + +import PingOrchestrate +import Foundation + +/// Protocol representing a Collector. +public protocol Collector: Action, Identifiable { + var id: UUID { get } + init(with json: [String: Any]) +} + + +extension ContinueNode { + /// Returns the list of collectors from the actions. + public var collectors: [any Collector] { + return actions.compactMap { $0 as? (any Collector) } + } +} + +/// Type alias for a list of collectors. +public typealias Collectors = [any Collector] + + +extension Collectors { + /// Finds the event type from a list of collectors. + ///This function iterates over the list of collectors and returns the value if the collector's value is not empty. + /// - Returns: The event type as a String if found, otherwise nil. + func eventType() -> String? { + for collector in self { + if let submitCollector = collector as? SubmitCollector, !submitCollector.value.isEmpty { + return submitCollector.value + } + if let flowCollector = collector as? FlowCollector, !flowCollector.value.isEmpty { + return flowCollector.value + } + } + return nil + } + + /// Represents a list of collectors as a JSON object for posting to the server. + /// This function takes a list of collectors and represents it as a JSON object. It iterates over the list of collectors, + /// adding each collector's key and value to the JSON object if the collector's value is not empty. + /// - Returns: JSON object representing the list of collectors. + func asJson() -> [String: Any] { + var jsonObject: [String: Any] = [:] + + for collector in self { + if let submitCollector = collector as? SubmitCollector, !submitCollector.value.isEmpty { + jsonObject[Constants.actionKey] = submitCollector.key + } + if let flowCollector = collector as? FlowCollector, !flowCollector.value.isEmpty { + jsonObject[Constants.actionKey] = flowCollector.key + } + } + + var formData: [String: Any] = [:] + for collector in self { + if let textCollector = collector as? TextCollector, !textCollector.value.isEmpty { + formData[textCollector.key] = textCollector.value + } + if let passwordCollector = collector as? PasswordCollector, !passwordCollector.value.isEmpty { + formData[passwordCollector.key] = passwordCollector.value + } + } + + jsonObject[Constants.formData] = formData + return jsonObject + } +} diff --git a/Davinci/Davinci/collector/CollectorFactory.swift b/Davinci/Davinci/collector/CollectorFactory.swift new file mode 100644 index 0000000..3d47a5b --- /dev/null +++ b/Davinci/Davinci/collector/CollectorFactory.swift @@ -0,0 +1,57 @@ +// +// CollectorFactory.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import PingOrchestrate + +/// The CollectorFactory singleton is responsible for creating and managing Collector instances. +/// It maintains a dictionary of collector creation functions, keyed by type. +/// It also provides functions to register new types of collectors and to create collectors from a JSON array. +public final class CollectorFactory { + // A dictionary to hold the collector creation functions. + var collectors: [String: any Collector.Type] = [:] + + /// The shared instance of the CollectorFactory. + public static let shared = CollectorFactory() + + init() { + register(type: Constants.TEXT, collector: TextCollector.self) + register(type: Constants.PASSWORD, collector: PasswordCollector.self) + register(type: Constants.SUBMIT_BUTTON, collector: SubmitCollector.self) + register(type: Constants.FLOW_BUTTON, collector: FlowCollector.self) + } + + /// Registers a new type of Collector. + /// - Parameters: + /// - type: The type of the Collector. + /// - block: A function that creates a new instance of the Collector. + public func register(type: String, collector: any Collector.Type) { + collectors[type] = collector + } + + /// Creates a list of Collector instances from an array of dictionaries. + /// Each dictionary should have a "type" field that matches a registered Collector type. + /// - Parameter array: The array of dictionaries to create the Collectors from. + /// - Returns: A list of Collector instances. + public func collector(from array: [[String: Any]]) -> Collectors { + var list: [any Collector] = [] + for item in array { + if let type = item[Constants.type] as? String, let collectorType = collectors[type] { + list.append(collectorType.init(with: item)) + } + } + return list + } + + /// Resets the CollectorFactory by clearing all registered collectors. + public func reset() { + collectors.removeAll() + } +} diff --git a/Davinci/Davinci/collector/FieldCollector.swift b/Davinci/Davinci/collector/FieldCollector.swift new file mode 100644 index 0000000..4042da8 --- /dev/null +++ b/Davinci/Davinci/collector/FieldCollector.swift @@ -0,0 +1,34 @@ +// +// FieldCollector.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Abstract class representing a field collector. +/// - property key: The key of the field collector. +/// - property label The label of the field collector. +/// - property value The value of the field collector. It's open for modification. +/// - property id The UUID of the field collector. +open class FieldCollector: Collector { + public var key: String = "" + public var label: String = "" + public var value: String = "" + public let id = UUID() + + /// Initializes a new instance of `FieldCollector`. + public init() {} + + /// Initializes a new instance of `FieldCollector`. + /// - Parameter json: The json to initialize from. + required public init(with json: [String: Any]) { + key = json[Constants.key] as? String ?? "" + label = json[Constants.label] as? String ?? "" + } +} diff --git a/Davinci/Davinci/collector/FlowCollector.swift b/Davinci/Davinci/collector/FlowCollector.swift new file mode 100644 index 0000000..325a189 --- /dev/null +++ b/Davinci/Davinci/collector/FlowCollector.swift @@ -0,0 +1,17 @@ +// +// FlowCollector.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Class representing a FlowCollector. +/// This class inherits from the FieldCollector class and implements the Collector protocol. +/// It is used to collect data in a flow. +public class FlowCollector: FieldCollector {} diff --git a/Davinci/Davinci/collector/Form.swift b/Davinci/Davinci/collector/Form.swift new file mode 100644 index 0000000..a73b2ca --- /dev/null +++ b/Davinci/Davinci/collector/Form.swift @@ -0,0 +1,31 @@ +// +// Form.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Class that handles the parsing and JSON representation of collectors. +/// This class provides functions to parse a JSON object into a list of collectors and to represent a list of collectors as a JSON object. +class Form { + /// Parses a JSON object into a list of collectors. + /// This function takes a JSON object and extracts the "form" field. It then iterates over the "fields" array in the "components" object, + /// parsing each field into a collector and adding it to a list. + /// - Parameter json :The JSON object to parse. + /// - Returns: A list of collectors parsed from the JSON object. + static func parse(json: [String: Any]) -> Collectors { + var collectors = Collectors() + if let form = json[Constants.form] as? [String: Any], + let components = form[Constants.components] as? [String: Any], + let fields = components[Constants.fields] as? [[String: Any]] { + collectors = CollectorFactory().collector(from: fields) + } + return collectors + } +} diff --git a/Davinci/Davinci/collector/PasswordCollector.swift b/Davinci/Davinci/collector/PasswordCollector.swift new file mode 100644 index 0000000..89ca5d5 --- /dev/null +++ b/Davinci/Davinci/collector/PasswordCollector.swift @@ -0,0 +1,29 @@ +// +// PasswordCollector.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingOrchestrate + +/// Class representing a PasswordCollector. +/// This class inherits from the FieldCollector class and implements the Closeable and Collector protocols. +/// It is used to collect password data. +public class PasswordCollector: FieldCollector, Closeable { + /// A flag to determine whether to clear the password or not after submission. + public var clearPassword: Bool = true + + /// Overrides the close function from the Closeable protocol. + /// It is used to clear the value of the password field when the collector is closed. + public func close() { + if clearPassword { + value = "" + } + } +} diff --git a/Davinci/Davinci/collector/SubmitCollector.swift b/Davinci/Davinci/collector/SubmitCollector.swift new file mode 100644 index 0000000..8f19b91 --- /dev/null +++ b/Davinci/Davinci/collector/SubmitCollector.swift @@ -0,0 +1,17 @@ +// +// SubmitCollector.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Class representing a TextCollector. +/// This class inherits from the FieldCollector class and implements the Collector protocol. +/// `SubmitCollector` is responsible for collecting and managing submission fields. +public class SubmitCollector: FieldCollector {} diff --git a/Davinci/Davinci/collector/TextCollector.swift b/Davinci/Davinci/collector/TextCollector.swift new file mode 100644 index 0000000..5fc3d9e --- /dev/null +++ b/Davinci/Davinci/collector/TextCollector.swift @@ -0,0 +1,17 @@ +// +// TextCollector.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Class representing a TextCollector. +/// This class inherits from the FieldCollector class and implements the Collector protocol. +/// It is used to collect text data. +public class TextCollector: FieldCollector {} diff --git a/Davinci/Davinci/module/Connector.swift b/Davinci/Davinci/module/Connector.swift new file mode 100644 index 0000000..bb677b9 --- /dev/null +++ b/Davinci/Davinci/module/Connector.swift @@ -0,0 +1,106 @@ +// +// Connector.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + + +import PingOrchestrate + +extension ContinueNode { + /// Extension property to get the id of a Connector. + public var id: String { + return (self as? Connector)?.idValue ?? "" + } + + /// Extension property to get the name of a Connector. + public var name: String { + return (self as? Connector)?.nameValue ?? "" + } + + /// Extension property to get the description of a Connector. + public var description: String { + return (self as? Connector)?.descriptionValue ?? "" + } + + /// Extension property to get the category of a Connector. + public var category: String { + return (self as? Connector)?.categoryValue ?? "" + } +} + + +/// Class representing a Connector. +///- property context: The FlowContext of the ContinueNode. +///- property davinci: The Davinci Flow of the ContinueNode. +///- property input: The input JsonObject of the ContinueNode. +///- property collectors: The collectors of the ContinueNode. +class Connector: ContinueNode { + + /// Initializer to create a new instance of Connector. + /// - Parameters: + /// - context: The FlowContext of the ContinueNode. + /// - davinci: The Davinci Flow of the ContinueNode. + /// - input: The input JsonObject of the ContinueNode. + /// - collectors: The collectors of the ContinueNode. + init(context: FlowContext, davinci: DaVinci, input: [String: Any], collectors: Collectors) { + super.init(context: context, workflow: davinci, input: input, actions: collectors) + } + + /// Function to convert the connector to a dictionary. + /// - returns: The connector as a JsonObject. + private func asJson() -> [String: Any] { + var parameters: [String: Any] = [:] + if let eventType = collectors.eventType() { + parameters[Constants.eventType] = eventType + } + parameters[Constants.data] = collectors.asJson() + + return [ + Constants.id: (input[Constants.id] as? String) ?? "", + Constants.eventName: (input[Constants.eventName] as? String) ?? "", + Constants.parameters: parameters + ] + } + + /// Lazy property to get the id of the connector. + lazy var idValue: String = { + return input[Constants.id] as? String ?? "" + }() + + /// Lazy property to get the name of the connector. + lazy var nameValue: String = { + guard let form = input[Constants.form] as? [String: Any] else { return "" } + return form[Constants.name] as? String ?? "" + }() + + /// Lazy property to get the description of the connector. + lazy var descriptionValue: String = { + guard let form = input[Constants.form] as? [String: Any] else { return "" } + return form[Constants.description] as? String ?? "" + }() + + /// Lazy property to get the category of the connector. + lazy var categoryValue: String = { + guard let form = input[Constants.form] as? [String: Any] else { return "" } + return form[Constants.category] as? String ?? "" + }() + + /// Function to convert the connector to a Request. + /// - Returns: The connector as a Request. + override func asRequest() -> Request { + let request = Request() + + let links: [String: Any]? = input[Constants._links] as? [String: Any] + let next = links?[Constants.next] as? [String: Any] + let href = next?[Constants.href] as? String ?? "" + + request.url(href) + request.header(name: Request.Constants.contentType, value: Request.ContentType.json.rawValue) + request.body(body: asJson()) + return request + } +} diff --git a/Davinci/Davinci/module/Oidc.swift b/Davinci/Davinci/module/Oidc.swift new file mode 100644 index 0000000..28292f3 --- /dev/null +++ b/Davinci/Davinci/module/Oidc.swift @@ -0,0 +1,89 @@ +// +// OIDC.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingOidc +import PingOrchestrate + +/// A module that integrates OIDC capabilities into the DaVinci workflow. +public class OidcModule { + + /// Initializes a new instance of `OidcModule`. + public init() {} + + /// The configuration for the OIDC module. + public static let config: Module = Module.of ({ OidcClientConfig() }) { setup in + + let config: OidcClientConfig = setup.config + let daVinciFlow: DaVinci = setup.workflow + + // Initializes the module. + setup.initialize { + // propagate the configuration from workflow to the module + config.httpClient = daVinciFlow.config.httpClient + config.logger = daVinciFlow.config.logger + // global context + daVinciFlow.sharedContext.set(key: SharedContext.Keys.oidcClientConfigKey, value: config) + //Override the agent setting + config.updateAgent(DefaultAgent()) + try await config.oidcInitialize() + } + + // Starts the module. + setup.start { context, request in + // When user starts the flow again, revoke previous token if exists + await daVinciFlow.user()?.revoke() + + let pkce = Pkce.generate() + context.flowContext.set(key: SharedContext.Keys.pkceKey, value: pkce) + return config.populateRequest(request: request, pkce: pkce) + } + + // Handles success of the module. + setup.success { context, success in + let cloneConfig: OidcClientConfig = config.clone() + + let flowPkce = context.flowContext.get(key: SharedContext.Keys.pkceKey) as? Pkce + let agent = CreateAgent(session: success.session, pkce: flowPkce) + cloneConfig.updateAgent(agent) + + let oidcuser: User = OidcUser(config: cloneConfig) + let prepareUser = UserDelegate(daVinci: daVinciFlow, user: oidcuser, session: success.session) + daVinciFlow.sharedContext.set(key: SharedContext.Keys.userKey, value: prepareUser) + + return SuccessNode(input: success.input, session: prepareUser) + } + + // Handles sign off of the module. + setup.signOff { request in + request.url(config.openId?.endSessionEndpoint ?? "") + + _ = await OidcClient(config: config).endSession { idToken in + request.parameter(name: OidcClient.Constants.id_token_hint, value: idToken) + request.parameter(name: OidcClient.Constants.client_id, value: config.clientId) + return true + } + + return request + } + } +} + +extension SharedContext.Keys { + /// The key used to store the PKCE value in the shared context. + public static let pkceKey = "com.pingidentity.davinci.PKCE" + + /// The key used to store the user in the shared context. + public static let userKey = "com.pingidentity.davinci.User" + + /// The key used to store the OIDC client configuration in the shared context. + public static let oidcClientConfigKey = "com.pingidentity.davinci.OidcClientConfig" +} diff --git a/Davinci/Davinci/module/Request.swift b/Davinci/Davinci/module/Request.swift new file mode 100644 index 0000000..b774436 --- /dev/null +++ b/Davinci/Davinci/module/Request.swift @@ -0,0 +1,76 @@ +// +// Request.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingOidc +import PingOrchestrate + +extension OidcClientConfig { + internal func populateRequest( + request: Request, + pkce: Pkce + ) -> Request { + request.url(openId?.authorizationEndpoint ?? "") + request.parameter(name: OidcClient.Constants.response_mode, value: "pi.flow") + request.parameter(name: OidcClient.Constants.client_id, value: clientId) + request.parameter(name: OidcClient.Constants.response_type, value: OidcClient.Constants.code) + request.parameter(name: OidcClient.Constants.scope, value: scopes.joined(separator: " ")) + request.parameter(name: OidcClient.Constants.redirect_uri, value: redirectUri) + request.parameter(name: OidcClient.Constants.code_challenge, value: pkce.codeChallenge) + request.parameter(name: OidcClient.Constants.code_challenge_method, value: pkce.codeChallengeMethod) + + if let acr = acrValues { + request.parameter(name: OidcClient.Constants.acr_values, value: acr) + } + + if let display = display { + request.parameter(name: OidcClient.Constants.display, value: display) + } + + for (key, value) in additionalParameters { + request.parameter(name: key, value: value) + } + + if let loginHint = loginHint { + request.parameter(name: OidcClient.Constants.login_hint, value: loginHint) + } + + if let nonce = nonce { + request.parameter(name: OidcClient.Constants.nonce, value: nonce) + } + + if let prompt = prompt { + request.parameter(name: OidcClient.Constants.prompt, value: prompt) + } + + if let uiLocales = uiLocales { + request.parameter(name: OidcClient.Constants.ui_locales, value: uiLocales) + } + + return request + } +} + + +extension OidcClient.Constants { + static let response_mode = "response_mode" + static let response_type = "response_type" + static let scope = "scope" + static let code_challenge = "code_challenge" + static let code_challenge_method = "code_challenge_method" + static let acr_values = "acr_values" + static let display = "display" + static let nonce = "nonce" + static let prompt = "prompt" + static let ui_locales = "ui_locales" + static let login_hint = "login_hint" + static let piflow = "pi.flow" +} diff --git a/Davinci/Davinci/module/Transform.swift b/Davinci/Davinci/module/Transform.swift new file mode 100644 index 0000000..e70f633 --- /dev/null +++ b/Davinci/Davinci/module/Transform.swift @@ -0,0 +1,123 @@ +// +// Transform.swift +// PingDavinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingOidc +import PingOrchestrate + +/// Module for transforming the response from DaVinci to `Node`. +public class NodeTransformModule { + + /// The module configuration for transforming the response from DaVinci to `Node`. + public static let config: Module = Module.of(setup: { setup in + setup.transform { flowContext, response in + let status = response.status() + + let body = response.body() + + // Check for 4XX errors that are unrecoverable + if (400..<500).contains(status) { + let json = try response.json(data: response.data) + let message = json[Constants.message] as? String ?? "" + + // Filter out client-side "timeout" related unrecoverable failures + if json[Constants.code] as? Int == Constants.code_1999 || json[Constants.code] as? String == Constants.requestTimedOut { + return FailureNode(cause: ApiError.error(status, json, body)) + } + + // Filter our "PingOne Authentication Connector" unrecoverable failures + if let connectorId = json[Constants.connectorId] as? String, connectorId == Constants.pingOneAuthenticationConnector, + let capabilityName = json[Constants.capabilityName] as? String, + [Constants.returnSuccessResponseRedirect, Constants.setSession].contains(capabilityName) { + return FailureNode(cause: ApiError.error(status, json, body)) + } + + // If we're still here, we have a 4XX failure that should be recoverable + return ErrorNode(status: status, input: json, message: message) + } + + // Handle success (2XX) responses + if status == 200 { + let json = try response.json(data: response.data) + + // Filter out 2XX errors with 'failure' status + if let failedStatus = json[Constants.status] as? String, failedStatus == Constants.FAILED { + return FailureNode(cause: ApiError.error(status, json, body)) + } + + // Filter out 2XX errors with error object + if let error = json[Constants.error] as? [String: Any], !error.isEmpty { + return FailureNode(cause: ApiError.error(status, json, body)) + } + + return transform(context: flowContext, davinci: setup.workflow, json: json) + } + + // Handle success (3XX) responses + if (300..<400).contains(status) { + let locationHeader = response.header(name: Constants.location) ?? "" + return FailureNode(cause: ApiError.error(status, [:], "Location: \(String(describing: locationHeader))" )) + } + + // 5XX errors are treated as unrecoverable failures + let json = try response.json(data: response.data) + return FailureNode(cause: ApiError.error(status, json, body)) + } + + }) + + private static func transform(context: FlowContext, davinci: DaVinci, json: [String: Any]) -> Node { + // If authorizeResponse is present, return success + if let _ = json[Constants.authorizeResponse] as? [String: Any] { + return SuccessNode(input: json, session: SessionResponse(json: json)) + } + + var collectors: Collectors = [] + if let _ = json[Constants.form] { + collectors.append(contentsOf: Form.parse(json: json)) + } + + return Connector(context: context, davinci: davinci, input: json, collectors: collectors) + } +} + + +/// Represents a session response parsed from a JSON object. +public struct SessionResponse: Session { + /// The raw JSON data of the session response. + public let json: [String: Any] + + /// Initializes a new session response with the given JSON data. + /// - Parameter json: The JSON data representing the session response. + public init(json: [String: Any] = [:]) { + self.json = json + } + + /// The session value extracted from the JSON response. + /// - Returns: A string representing the session code or an empty string if not available. + public var value: String { + get { + let authResponse = json[Constants.authorizeResponse] as? [String: Any] + return authResponse?[Constants.code] as? String ?? "" + } + } +} + + +/// Represents API errors that occur during response transformation. +public enum ApiError: Error { + /// An error containing an HTTP status code, a JSON object, and a descriptive message. + /// - Parameters: + /// - status: The HTTP status code of the error. + /// - json: The JSON data associated with the error. + /// - message: A descriptive message explaining the error. + case error(Int, [String: Any], String) +} diff --git a/Davinci/DavinciTests/CallbackFactoryTests.swift b/Davinci/DavinciTests/CallbackFactoryTests.swift new file mode 100644 index 0000000..5b487f6 --- /dev/null +++ b/Davinci/DavinciTests/CallbackFactoryTests.swift @@ -0,0 +1,70 @@ +// +// CallbackFactoryTests.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingDavinci + +class CallbackFactoryTests: XCTestCase { + override func setUp() { + CollectorFactory.shared.register(type: "type1", collector: DummyCallback.self) + CollectorFactory.shared.register(type: "type2", collector: Dummy2Callback.self) + } + + func testShouldReturnListOfCollectorsWhenValidTypesAreProvided() { + let jsonArray: [[String: Any]] = [ + ["type": "type1"], + ["type": "type2"] + ] + + let callbacks = CollectorFactory.shared.collector(from: jsonArray) + XCTAssertEqual((callbacks[0] as? DummyCallback)?.value, "dummy") + XCTAssertEqual((callbacks[1] as? Dummy2Callback)?.value, "dummy2") + + XCTAssertEqual(callbacks.count, 2) + } + + func testShouldReturnEmptyListWhenNoValidTypesAreProvided() { + let jsonArray: [[String: Any]] = [ + ["type": "invalidType"] + ] + + let callbacks = CollectorFactory.shared.collector(from: jsonArray) + + XCTAssertTrue(callbacks.isEmpty) + } + + func testShouldReturnEmptyListWhenJsonArrayIsEmpty() { + let jsonArray: [[String: Any]] = [] + + let callbacks = CollectorFactory.shared.collector(from: jsonArray) + + XCTAssertTrue(callbacks.isEmpty) + } +} + +class DummyCallback: Collector { + var id: UUID = UUID() + var value: String? + + required public init(with json: [String: Any]) { + value = "dummy" + } +} + +class Dummy2Callback: Collector { + var id: UUID = UUID() + var value: String? + + required public init(with json: [String: Any]) { + value = "dummy2" + } +} diff --git a/Davinci/DavinciTests/CollectorRegistryTests.swift b/Davinci/DavinciTests/CollectorRegistryTests.swift new file mode 100644 index 0000000..76ce17f --- /dev/null +++ b/Davinci/DavinciTests/CollectorRegistryTests.swift @@ -0,0 +1,57 @@ +// +// CollectorRegistryTests.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingDavinci + +class CollectorRegistryTests: XCTestCase { + + private var collectorFactory: CollectorFactory! + + override func setUp() { + super.setUp() + collectorFactory = CollectorFactory() + } + + override func tearDown() { + super.tearDown() + collectorFactory.reset() + } + + func testShouldRegisterCollector() { + let jsonArray: [[String: Any]] = [ + ["type": "TEXT"], + ["type": "PASSWORD"], + ["type": "SUBMIT_BUTTON"], + ["type": "FLOW_BUTTON"] + ] + + let collectors = collectorFactory.collector(from: jsonArray) + XCTAssertTrue(collectors[0] is TextCollector) + XCTAssertTrue(collectors[1] is PasswordCollector) + XCTAssertTrue(collectors[2] is SubmitCollector) + XCTAssertTrue(collectors[3] is FlowCollector) + } + + func testShouldIgnoreUnknownCollector() { + let jsonArray: [[String: Any]] = [ + ["type": "TEXT"], + ["type": "PASSWORD"], + ["type": "SUBMIT_BUTTON"], + ["type": "FLOW_BUTTON"], + ["type": "UNKNOWN"] + ] + + let collectors = collectorFactory.collector(from: jsonArray) + XCTAssertEqual(collectors.count, 4) + } +} diff --git a/Davinci/DavinciTests/DaVinciErrorTests.swift b/Davinci/DavinciTests/DaVinciErrorTests.swift new file mode 100644 index 0000000..e7064a2 --- /dev/null +++ b/Davinci/DavinciTests/DaVinciErrorTests.swift @@ -0,0 +1,591 @@ +// +// DaVinciErrorTests.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingOrchestrate +@testable import PingStorage +@testable import PingLogger +@testable import PingOidc +@testable import PingDavinci + +class DaVinciErrorTests: XCTestCase { + + override func setUp() { + super.setUp() + MockURLProtocol.startInterceptingRequests() + _ = CollectorFactory() + } + + override func tearDown() { + super.tearDown() + MockURLProtocol.stopInterceptingRequests() + } + + func testDaVinciWellKnownEndpointFailedwith404() async throws { + + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 404, httpVersion: nil, headerFields: MockResponse.headers)!, "Not Found".data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let error = (node as! FailureNode).cause as! OidcError + + switch error { + case .apiError(let code, _): + XCTAssertEqual(code, 404) + default: + XCTFail() + } + + } + + func testDaVinciAuthorizeEndpointFailedWith401() async throws { + let number = Int.random(in: 400 ..< 500) + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: number, httpVersion: nil, headerFields: MockResponse.headers)!, """ + { + "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc", + "code": 1999, + "message": "Unauthorized!", + "errorMessage": "Unauthorized!", + } + """.data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let failureNode = node as! FailureNode + let apiError = failureNode.cause as! ApiError + switch apiError { + case .error(let code, _, _): + XCTAssertTrue(code == number) + } + } + + func testDaVinciInvalidSessionBetween400To499() async throws { + let number = Int.random(in: 400 ..< 500) + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: number, httpVersion: nil, headerFields: MockResponse.headers)!, """ + { + "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc", + "connectorId": "pingOneAuthenticationConnector", + "capabilityName": "setSession", + "message": "Invalid Connector.", + } + """.data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let failureNode = node as! FailureNode + let apiError = failureNode.cause as! ApiError + switch apiError { + case .error(let code, _, _): + XCTAssertTrue(code == number) + } + } + + func testDaVinciInvalidConnectorBetween400To499() async throws { + let number = Int.random(in: 400 ..< 500) + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: number, httpVersion: nil, headerFields: MockResponse.headers)!, """ + { + "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc", + "connectorId": "pingOneAuthenticationConnector", + "capabilityName": "returnSuccessResponseRedirect", + "message": "Invalid Connector.", + } + """.data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let failureNode = node as! FailureNode + let apiError = failureNode.cause as! ApiError + switch apiError { + case .error(let code, _, _): + XCTAssertTrue(code == number) + } + } + + func testDaVinciTimeOutBetween400To499() async throws { + let number = Int.random(in: 400 ..< 500) + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: number, httpVersion: nil, headerFields: MockResponse.headers)!, """ + { + "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc", + "code": "requestTimedOut", + "message": "Request timed out. Please try again.", + } + """.data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let failureNode = node as! FailureNode + let apiError = failureNode.cause as! ApiError + switch apiError { + case .error(let code, _, _): + XCTAssertTrue(code == number) + } + } + + + func testDaVinciAuthorizeEndpointFailedBetween400To499() async throws { + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.headers)!, """ + { + "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc", + "code": "INVALID_REQUEST", + "message": "Invalid DV Flow Policy ID: Single_Factor" + } + """.data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is ErrorNode) + let errorNode = node as! ErrorNode + XCTAssertTrue(errorNode.input.description.contains("INVALID_REQUEST")) + } + + func testDaVinciAuthorizeEndpointFailedWith500() async throws { + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 500, httpVersion: nil, headerFields: MockResponse.headers)!, """ + { + "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc", + "code": "INVALID_REQUEST", + "message": "Invalid DV Flow Policy ID: Single_Factor" + } + """.data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let failureNode = node as! FailureNode + let apiError = failureNode.cause as! ApiError + switch apiError { + case .error(let code, _, _): + XCTAssertTrue(code == 500) + } + + } + + func testDaVinciAuthorizeEndpointFailedWithOKResponseButFailedStatusDuringTransform() async throws { + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, """ + { + "environment": { + "id": "0c6851ed-0f12-4c9a-a174-9b1bf8b438ae" + }, + "status": "FAILED", + "error": { + "code": "login_required", + "message": "The request could not be completed. There was an issue processing the request" + } + } + """.data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let error = (node as! FailureNode).cause as! ApiError + + switch error { + case .error( _, _, let message): + XCTAssertTrue(message.contains("login_required")) + } + + } + + func testDaVinciAuthorizeEndpointFailedWithOKResponseButErrorDuringTransform() async throws { + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, """ + { + "environment": { + "id": "0c6851ed-0f12-4c9a-a174-9b1bf8b438ae" + }, + "error": { + "code": "login_required", + "message": "The request could not be completed. There was an issue processing the request" + } + } + """.data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let error = (node as! FailureNode).cause as! ApiError + + switch error { + case .error( _, _, let message): + XCTAssertTrue(message.contains("login_required")) + } + + } + + func testDaVinciTransformFailed() async throws { + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.authorizeResponseHeaders)!, " Not a Json ".data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + } + + func testDaVinciInvalidPassword() async throws { + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.customHTMLTemplate.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.customHTMLTemplate.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.customHTMLTemplateHeaders)!, MockResponse.customHTMLTemplateWithInvalidPassword) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.authorizeResponseHeaders)!, MockResponse.authorizeResponse) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + let connector = node as! ContinueNode + if let textCollector = connector.collectors[0] as? TextCollector { + textCollector.value = "My First Name" + } + if let passwordCollector = connector.collectors[1] as? PasswordCollector { + passwordCollector.value = "My Password" + } + if let submitCollector = connector.collectors[2] as? SubmitCollector { + submitCollector.value = "click me" + } + + let next = await connector.next() + + XCTAssertEqual((connector.collectors[1] as? PasswordCollector)?.value, "") + + XCTAssertTrue(next is ErrorNode) + let errorNode = next as! ErrorNode + XCTAssertEqual(errorNode.message, "Invalid username and/or password") + XCTAssertTrue(errorNode.input.description.contains("The provided password did not match provisioned password")) + } + + func testDaVinci3xxErrorWithLocationHeaderInResponse() async throws { + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.authorization.url.path: + var headers = MockResponse.authorizeResponseHeaders + headers["Location"] = "https://apps.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/signon/?error=test" + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 302, httpVersion: nil, headerFields: headers)!, " ".data(using: .utf8)!) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + let node = await daVinci.start() + XCTAssertTrue(node is FailureNode) + let failureNode = node as! FailureNode + let apiError = failureNode.cause as! ApiError + switch apiError { + case .error(let code, _, let message): + XCTAssertEqual(message, "Location: https://apps.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/signon/?error=test") + XCTAssertTrue(code == 302) + } + } +} diff --git a/Davinci/DavinciTests/DaVinciTests.swift b/Davinci/DavinciTests/DaVinciTests.swift new file mode 100644 index 0000000..e30cbf1 --- /dev/null +++ b/Davinci/DavinciTests/DaVinciTests.swift @@ -0,0 +1,309 @@ +// +// DaVinciTests.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOrchestrate +@testable import PingLogger +@testable import PingOidc +@testable import PingStorage +@testable import PingDavinci + +final class DaVinciTests: XCTestCase { + + override func setUp() { + super.setUp() + MockURLProtocol.startInterceptingRequests() + _ = CollectorFactory() + + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse) + case MockAPIEndpoint.token.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenResponse) + case MockAPIEndpoint.userinfo.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.userinfo.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.userinfoResponse) + case MockAPIEndpoint.revocation.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.revocation.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, Data()) + case MockAPIEndpoint.endSession.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.endSession.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, Data()) + case MockAPIEndpoint.customHTMLTemplate.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.customHTMLTemplate.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.customHTMLTemplateHeaders)!, MockResponse.customHTMLTemplate) + case MockAPIEndpoint.authorization.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.authorizeResponseHeaders)!, MockResponse.authorizeResponse) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + } + + override func tearDown() { + super.tearDown() + MockURLProtocol.stopInterceptingRequests() + } + + func testDaVinci() throws { + + let davinci = DaVinci.createDaVinci { config in + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1" + oidcValue.scopes = ["openid", "email", "address", "phone", "profile"] + oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" + oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration" + } + } + + XCTAssertEqual(davinci.config.modules.count, 4) + XCTAssertEqual(davinci.initHandlers.count, 2) + XCTAssertEqual(davinci.nextHandlers.count, 2) + XCTAssertEqual(davinci.nodeHandlers.count, 0) + XCTAssertEqual(davinci.responseHandlers.count, 1) + XCTAssertEqual(davinci.signOffHandlers.count, 2) + XCTAssertEqual(davinci.successHandlers.count, 1) + + let nosession = Module.of { setup in + setup.next { ( context,connector, request) in + request.header(name: "nosession", value: "true") + return request + } + } + + let davinci1 = DaVinci.createDaVinci { config in + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1" + oidcValue.scopes = ["openid", "email", "address", "phone", "profile"] + oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" + oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration" + } + + config.module(nosession) + } + + XCTAssertEqual(davinci1.config.modules.count, 5) + XCTAssertEqual(davinci1.initHandlers.count, 2) + XCTAssertEqual(davinci1.nextHandlers.count, 3) + XCTAssertEqual(davinci1.nodeHandlers.count, 0) + XCTAssertEqual(davinci1.responseHandlers.count, 1) + XCTAssertEqual(davinci1.signOffHandlers.count, 2) + XCTAssertEqual(davinci1.successHandlers.count, 1) + } + + + func testDaVinciDefaultModuleSequence() async throws { + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + XCTAssertEqual(4, daVinci.config.modules.count) + let list = daVinci.config.modules + XCTAssertTrue(list[0].config is CustomHeaderConfig) + XCTAssertTrue(list[1].config is Void) + XCTAssertTrue(list[2].config is OidcClientConfig) + XCTAssertTrue(list[3].config is CookieConfig) + + } + + func testDaVinciSimpleHappyPath() async throws { + let tokenStorage = MemoryStorage() + let cookieStorage = MemoryStorage<[CustomHTTPCookie]>() + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = tokenStorage + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = cookieStorage + cookieValue.persist = ["ST"] + } + } + + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + let continueNode = node as! ContinueNode + XCTAssertEqual(continueNode.collectors.count, 5) + + XCTAssertEqual(continueNode.id, "cq77vwelou") + XCTAssertEqual(continueNode.name, "Username/Password Form") + XCTAssertEqual(continueNode.description, "Test Description") + XCTAssertEqual(continueNode.category, "CUSTOM_HTML") + + (continueNode.collectors[0] as? TextCollector)?.value = "My First Name" + (continueNode.collectors[1] as? PasswordCollector)?.value = "My Password" + (continueNode.collectors[2] as? SubmitCollector)?.value = "click me" + + node = await continueNode.next() + XCTAssertTrue(node is SuccessNode) + + let authorizeReq = MockURLProtocol.requestHistory[1] + XCTAssertTrue(authorizeReq.url!.query?.contains("client_id=test") ?? false) + XCTAssertTrue(authorizeReq.url!.query?.contains("response_mode=pi.flow") ?? false) + XCTAssertTrue(authorizeReq.url!.query?.contains("code_challenge_method=S256") ?? false) + XCTAssertTrue(authorizeReq.url!.query?.contains("code_challenge=") ?? false) + XCTAssertTrue(authorizeReq.url!.query?.contains("redirect_uri=") ?? false) + + let request = MockURLProtocol.requestHistory[2] + //let result = request.httpBody as! TextContent + // let json = try JSONSerialization.jsonObject(with: request.httpBody!) as! [String: Any] + // XCTAssertEqual(json["eventName"] as? String, "continue") + // let parameters = json["parameters"] as! [String: Any] + // let data = parameters["data"] as! [String: Any] + // XCTAssertEqual(data["actionKey"] as? String, "SIGNON") + // let formData = data["formData"] as! [String: Any] + // XCTAssertEqual(formData["username"] as? String, "My First Name") + // XCTAssertEqual(formData["password"] as? String, "My Password") + + XCTAssertEqual(request.allHTTPHeaderFields!["x-requested-with"], "ping-sdk") + XCTAssertEqual(request.allHTTPHeaderFields!["x-requested-platform"], "ios") + XCTAssertTrue(request.allHTTPHeaderFields!["Cookie"]?.contains("interactionId") ?? false) + //XCTAssertTrue(request.allHTTPHeaderFields!["Cookie"]?.contains("interactionToken") ?? false) + //XCTAssertTrue(request.allHTTPHeaderFields!["Cookie"]?.contains("skProxyApiEnvironmentId") ?? false) + + let successNode = node as! SuccessNode + let user = successNode.user + let userToken = await user?.token() + switch userToken! { + case .success(let token): + XCTAssertEqual(token.accessToken, "Dummy AccessToken") + break + case .failure(_): + XCTFail("Should have succeeded") + } + // XCTAssertEqual((user?.token() as? Result.Success)?.value.accessToken, "Dummy AccessToken") + + let u = await daVinci.user() + await u?.logout() + let revoke = MockURLProtocol.requestHistory[4] + XCTAssertEqual(revoke.url!.absoluteString, "https://auth.test-one-pingone.com/revoke") + // let revokeBody = try JSONSerialization.jsonObject(with: revoke.httpBody!) as! [String: Any] + // XCTAssertEqual(revokeBody["client_id"] as? String, "test") + // XCTAssertEqual(revokeBody["token"] as? String, "Dummy RefreshToken") + + let signOff = MockURLProtocol.requestHistory[5] + XCTAssertEqual(signOff.url!.absoluteString, "https://auth.test-one-pingone.com/signoff?id_token_hint=Dummy%20IdToken&client_id=test") + XCTAssertTrue(signOff.allHTTPHeaderFields!["Cookie"]?.contains("ST=session_token") ?? false) + + let storedToken = try await tokenStorage.get() + XCTAssertNil(storedToken) + let storedCokie = try await cookieStorage.get() + XCTAssertNil(storedCokie) + let storedUser = await daVinci.user() + XCTAssertNil(storedUser) + } + + func testDaVinciAdditionOidcParameter() async throws { + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = MemoryStorage() + oidcValue.logger = LogManager.standard + oidcValue.acrValues = "acrValues" + oidcValue.display = "display" + oidcValue.loginHint = "login_hint" + oidcValue.nonce = "nonce" + oidcValue.prompt = "prompt" + oidcValue.uiLocales = "ui_locales" + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = MemoryStorage() + cookieValue.persist = ["ST"] + } + } + + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + let connector = node as! ContinueNode + (connector.collectors[0] as? TextCollector)?.value = "My First Name" + (connector.collectors[1] as? PasswordCollector)?.value = "My Password" + (connector.collectors[2] as? SubmitCollector)?.value = "click me" + + node = await connector.next() + XCTAssertTrue(node is SuccessNode) + + let authorizeReq = MockURLProtocol.requestHistory[1] + XCTAssertTrue(authorizeReq.url?.query?.contains("client_id=test") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("response_mode=pi.flow") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("code_challenge_method=S256") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("code_challenge=") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("redirect_uri=http://localhost:8080") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("acr_values=acrValues") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("display=display") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("login_hint=login_hint") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("nonce=nonce") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("prompt=prompt") ?? false) + XCTAssertTrue(authorizeReq.url?.query?.contains("ui_locales=ui_locales") ?? false) + } + + func testDaVinciRevokeAccessToken() async throws { + let tokenStorage = MemoryStorage() + let cookieStorage = MemoryStorage<[CustomHTTPCookie]>() + let daVinci = DaVinci.createDaVinci { config in + config.httpClient = HttpClient(session: .shared) + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "http://localhost:8080" + oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration" + oidcValue.storage = tokenStorage + oidcValue.logger = LogManager.standard + } + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = cookieStorage + cookieValue.persist = ["ST"] + } + } + + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + let connector = node as! ContinueNode + (connector.collectors[0] as? TextCollector)?.value = "My First Name" + (connector.collectors[1] as? PasswordCollector)?.value = "My Password" + (connector.collectors[2] as? SubmitCollector)?.value = "click me" + + node = await connector.next() + XCTAssertTrue(node is SuccessNode) + + let u = await daVinci.user() + await u?.revoke() + let storedToken = try await tokenStorage.get() + XCTAssertNil(storedToken) + let storedCokie = try await cookieStorage.get() + XCTAssertNotNil(storedCokie) + } +} diff --git a/Davinci/DavinciTests/FieldCollectorTests.swift b/Davinci/DavinciTests/FieldCollectorTests.swift new file mode 100644 index 0000000..c5bb125 --- /dev/null +++ b/Davinci/DavinciTests/FieldCollectorTests.swift @@ -0,0 +1,38 @@ +// +// FieldCollectorTests.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingDavinci + +class FieldCollectorTests: XCTestCase { + + class MockFieldCollector: FieldCollector {} + + func testShouldInitializeKeyAndLabelFromJsonObject() { + + let jsonObject: [String: String] = [ + "key": "testKey", + "label": "testLabel" + ] + + let fieldCollector = MockFieldCollector(with: jsonObject) + + XCTAssertEqual("testKey", fieldCollector.key) + XCTAssertEqual("testLabel", fieldCollector.label) + } + + func testShouldReturnValueWhenValueIsSet() { + let fieldCollector = MockFieldCollector() + fieldCollector.value = "test" + XCTAssertEqual("test", fieldCollector.value) + } +} diff --git a/Davinci/DavinciTests/integration tests/DaVinciIntegrationTests.swift b/Davinci/DavinciTests/integration tests/DaVinciIntegrationTests.swift new file mode 100644 index 0000000..4faccc5 --- /dev/null +++ b/Davinci/DavinciTests/integration tests/DaVinciIntegrationTests.swift @@ -0,0 +1,863 @@ +// +// DaVinciIntegrationTests.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import XCTest +@testable import PingOrchestrate +@testable import PingLogger +@testable import PingOidc +@testable import PingStorage +@testable import PingDavinci + +class DaVinciIntegrationTests: XCTestCase { + private var daVinci: DaVinci! + private var username: String! + private var userFname: String! + private var userLname: String! + private var password: String! + private var newPassword: String! + private var verificationCode: String! + + override func setUp() async throws { + try await super.setUp() + + username = "e2euser@example.com" + userFname = "E2E" + userLname = "iOS" + password = "Demo1234#1" + newPassword = "New1234#1" + verificationCode = "1234" + + daVinci = DaVinci.createDaVinci { config in + config.logger = LogManager.standard + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "021b83ce-a9b1-4ad4-8c1d-79e576eeab76" + oidcValue.scopes = ["openid", "email", "address", "phone", "profile"] + oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" + oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration" + } + } + + // Start with a clean session + await daVinci.user()?.logout() + } + + // TestRailCase(21274) + func testLoginSuccess() async throws { + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + var continueNode = node as! ContinueNode + + // Login form validation... + XCTAssertEqual(continueNode.collectors.count, 5) + XCTAssertTrue(continueNode.collectors[0] is TextCollector) + XCTAssertTrue(continueNode.collectors[1] is PasswordCollector) + XCTAssertTrue(continueNode.collectors[2] is SubmitCollector) + XCTAssertTrue(continueNode.collectors[3] is FlowCollector) + XCTAssertTrue(continueNode.collectors[4] is FlowCollector) + + XCTAssertEqual("E2E Login Form", continueNode.name) + XCTAssertEqual("Enter your username and password", continueNode.description) + + (continueNode.collectors[0] as? TextCollector)?.value = username + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" // This will submit the form... + node = await continueNode.next() + XCTAssertTrue(node is ContinueNode) + continueNode = node as! ContinueNode + + // Verify the Successful login form + XCTAssertTrue(continueNode.collectors.count == 3) + XCTAssertTrue(continueNode.collectors[0] is SubmitCollector) + XCTAssertTrue(continueNode.collectors[1] is FlowCollector) + XCTAssertTrue(continueNode.collectors[2] is FlowCollector) + + XCTAssertEqual("Successful login", continueNode.name) + XCTAssertEqual("Successfully logged in to DaVinci", continueNode.description) + XCTAssertEqual("Continue", (continueNode.collectors[0] as! SubmitCollector).label) + XCTAssertEqual("Reset password...", (continueNode.collectors[1] as! FlowCollector).label) + XCTAssertEqual("Delete user...", (continueNode.collectors[2] as! FlowCollector).label) + + // Click continue + (continueNode.collectors[0] as! SubmitCollector).value = "Continue" + + node = await continueNode.next() + XCTAssertTrue(node is SuccessNode) + let successNode = node as! SuccessNode + + let user = successNode.user + let userToken = await user?.token() + switch userToken! { + case .success(let token): + XCTAssertNotNil(token.accessToken) + break + case .failure(_): + XCTFail("Should have succeeded") + } + + let u = await daVinci.user() + await u?.logout() ?? { XCTFail("User is null") }() + + // After logout make sure the user is null + let daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + } + + // TestRailCase(21275) + func testLoginFailure() async throws { + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + let continueNode = node as! ContinueNode + + // Make sure that we are at the Login form... + XCTAssertEqual("E2E Login Form", continueNode.name) + + (continueNode.collectors[0] as? TextCollector)?.value = username + (continueNode.collectors[1] as? PasswordCollector)?.value = "invalid" + (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" + node = await continueNode.next() + XCTAssertTrue(node is ErrorNode) + let errorNode = node as! ErrorNode + + // Verify the error message upon attempt to login with invalid credentials + XCTAssertEqual("Invalid username and/or password", errorNode.message) + + let daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + } + + // TestRailCase(21276) + func testActiveSession() async throws { + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + var continueNode = node as! ContinueNode + + // Ensure that we are at the Login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Login with valid credentials... + (continueNode.collectors[0] as? TextCollector)?.value = username + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // Verify the user was successfully logged in + XCTAssertEqual("Successful login", continueNode.name) + + // Click continue + (continueNode.collectors[0] as! SubmitCollector).value = "Continue" + + node = await continueNode.next() + XCTAssertTrue(node is SuccessNode) + var successNode = node as! SuccessNode + + var user = successNode.user + var userToken = await user?.token() + switch userToken! { + case .success(let token): + XCTAssertNotNil(token.accessToken) + break + case .failure(_): + XCTFail("Should have succeeded") + } + + // Launch the login form again (active session exists...) + // Should go directly to success... + let node1 = await daVinci.start() + XCTAssertTrue(node1 is SuccessNode) + successNode = node1 as! SuccessNode + + user = successNode.user + userToken = await user?.token() + switch userToken! { + case .success(let token): + XCTAssertNotNil(token.accessToken) + break + case .failure(_): + XCTFail("Should have succeeded") + } + + let u = await daVinci.user() + await u?.logout() ?? { XCTFail("User is null") }() + + // After logout make sure the user is null + let daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + } + + // TestRailCase(21253) + func testUserRegistrationSuccess() async throws { + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Click the registration link + (continueNode.collectors[3] as? FlowCollector)?.value = "register" + node = await continueNode.next() + XCTAssertTrue(node is ContinueNode) + continueNode = node as! ContinueNode + + // Validate the registration form + XCTAssertTrue(continueNode.collectors.count == 6) + XCTAssertEqual("Registration Form", continueNode.name) + XCTAssertEqual("Collect Name, Email, Password", continueNode.description) + + XCTAssertTrue(continueNode.collectors[0] is TextCollector) // Email + XCTAssertTrue(continueNode.collectors[1] is PasswordCollector) // Password + XCTAssertTrue(continueNode.collectors[2] is TextCollector) // Given Name + XCTAssertTrue(continueNode.collectors[3] is TextCollector) // Family Name + XCTAssertTrue(continueNode.collectors[4] is SubmitCollector) // Continue + XCTAssertTrue(continueNode.collectors[5] is FlowCollector) // Already have an account (link) + + XCTAssertEqual("Email", (continueNode.collectors[0] as! TextCollector).label) + XCTAssertEqual("Password", (continueNode.collectors[1] as! PasswordCollector).label) + XCTAssertEqual("Given Name", (continueNode.collectors[2] as! TextCollector).label) + XCTAssertEqual("Family Name", (continueNode.collectors[3] as! TextCollector).label) + XCTAssertEqual("Continue", (continueNode.collectors[4] as! SubmitCollector).label) + XCTAssertEqual("Already have an account? Sign On", (continueNode.collectors[5] as! FlowCollector).label) + + // Fill in the registration form + let date = Date() + let formatter = DateFormatter() + formatter.dateFormat = "HHmmssSSSS" + + let newUser = "e2e" + formatter.string(from: date) + "@example.com" + + (continueNode.collectors[0] as? TextCollector)?.value = newUser + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? TextCollector)?.value = userFname + (continueNode.collectors[3] as? TextCollector)?.value = userLname + (continueNode.collectors[4] as? SubmitCollector)?.value = "Save" + + // Click continue + (continueNode.collectors[4] as! SubmitCollector).value = "Save" + + node = await continueNode.next() + continueNode = node as! ContinueNode + + // User should be navigated to the verification code screen + XCTAssertTrue(continueNode.collectors.count == 3 ) + XCTAssertTrue(continueNode.collectors[0] is TextCollector) + XCTAssertTrue(continueNode.collectors[1] is SubmitCollector) + XCTAssertTrue(continueNode.collectors[2] is FlowCollector) + + XCTAssertEqual("Enter verification code", continueNode.name) + XCTAssertEqual("Hint: The verification code is 1234", continueNode.description) + XCTAssertEqual("Verification Code", (continueNode.collectors[0] as! TextCollector).label) + XCTAssertEqual("Verify", (continueNode.collectors[1] as! SubmitCollector).label) + XCTAssertEqual("Resend Verification Code", (continueNode.collectors[2] as! FlowCollector).label) + + // Fill in the verification code and submit + (continueNode.collectors[0] as? TextCollector)?.value = verificationCode + (continueNode.collectors[1] as? SubmitCollector)?.value = "Verify" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // User should be navigated to the "Successful user creation" screen... + XCTAssertTrue(continueNode.collectors.count == 1 ) + XCTAssertTrue(continueNode.collectors[0] is SubmitCollector) + + XCTAssertEqual("Registration Complete", continueNode.name) + XCTAssertEqual("Notify User Account Is Successfully Created", continueNode.description) + XCTAssertEqual("Continue", (continueNode.collectors[0] as! SubmitCollector).label) + + // Click "Continue" to finish the registration process + (continueNode.collectors[0] as? SubmitCollector)?.value = "Continue" + node = await continueNode.next() + XCTAssertTrue(node is SuccessNode) + let successNode = node as! SuccessNode + + let user = successNode.user + let userToken = await user?.token() + switch userToken! { + case .success(let token): + XCTAssertNotNil(token.accessToken) + break + case .failure(_): + XCTFail("Should have succeeded") + } + + let u = await daVinci.user() + await u?.logout() ?? { XCTFail("User is null") }() + + // After logout make sure the user is null + let daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + + // Delete the user from PingOne + try await deleteUser(userName: newUser, pass: password) + } + + // TestRailCase(21269) + func testUserRegistrationFailureUserAlreadyExists() async throws { + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Click the registration link + (continueNode.collectors[3] as? FlowCollector)?.value = "register" + node = await continueNode.next() + XCTAssertTrue(node is ContinueNode) + continueNode = node as! ContinueNode + + // Make sure that we are at the registration form + XCTAssertEqual("Registration Form", continueNode.name) + + // Fill the registration form with username that already exists + (continueNode.collectors[0] as? TextCollector)?.value = username + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? TextCollector)?.value = userFname + (continueNode.collectors[3] as? TextCollector)?.value = userLname + (continueNode.collectors[4] as? SubmitCollector)?.value = "Save" + + // Click continue + (continueNode.collectors[4] as! SubmitCollector).value = "Save" + + node = await continueNode.next() + let errorNode = node as! ErrorNode + + // Make sure we get the expected error + XCTAssertEqual("uniquenessViolation", String(describing: errorNode.input["code"]!)) + XCTAssertEqual("400", String(describing: errorNode.input["httpResponseCode"]!)) + XCTAssertEqual("An account with that email address already exists.", errorNode.message) + + // Make sure that we are still at the registration form + XCTAssertEqual("Registration Form", continueNode.name) + } + + // TestRailCase(21270) + func testUserRegistrationFailureInvalidEmail() async throws { + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Click the registration link + (continueNode.collectors[3] as? FlowCollector)?.value = "register" + node = await continueNode.next() + XCTAssertTrue(node is ContinueNode) + continueNode = node as! ContinueNode + + // Make sure that we are at the registration form + XCTAssertEqual("Registration Form", continueNode.name) + + // Enter invalid (empty) email in the registration form + (continueNode.collectors[0] as? TextCollector)?.value = "" + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? TextCollector)?.value = userFname + (continueNode.collectors[3] as? TextCollector)?.value = userLname + (continueNode.collectors[4] as? SubmitCollector)?.value = "Save" + + // Click continue + (continueNode.collectors[4] as! SubmitCollector).value = "Save" + + node = await continueNode.next() + let errorNode = node as! ErrorNode + + // Make sure we get the expected error + XCTAssertEqual("invalidInput", String(describing: errorNode.input["code"]!)) + XCTAssertEqual("400", String(describing: errorNode.input["httpResponseCode"]!)) + XCTAssertEqual("Enter a valid email address", errorNode.message) + + // Make sure that we are still at the registration form + XCTAssertEqual("Registration Form", continueNode.name) + } + + // TestRailCase(21272) + func testUserRegistrationFailureInvalidPassword() async throws { + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Click the registration link + (continueNode.collectors[3] as? FlowCollector)?.value = "register" + node = await continueNode.next() + XCTAssertTrue(node is ContinueNode) + continueNode = node as! ContinueNode + + // Make sure that we are at the registration form + XCTAssertEqual("Registration Form", continueNode.name) + + let date = Date() + let formatter = DateFormatter() + formatter.dateFormat = "HHmmssSSSS" + + let newUser = "e2e" + formatter.string(from: date) + "@example.com" + + // Enter invalid password in the registration form + (continueNode.collectors[0] as? TextCollector)?.value = newUser + // Note: The password rules for the E2E Tests population require at least one number + (continueNode.collectors[1] as? PasswordCollector)?.value = "invalid" + (continueNode.collectors[2] as? TextCollector)?.value = userFname + (continueNode.collectors[3] as? TextCollector)?.value = userLname + (continueNode.collectors[4] as? SubmitCollector)?.value = "Save" + + // Click continue + (continueNode.collectors[4] as! SubmitCollector).value = "Save" + + node = await continueNode.next() + let errorNode = node as! ErrorNode + + // Make sure we get the expected error + XCTAssertEqual("invalidValue", String(describing: errorNode.input["code"]!)) + XCTAssertEqual("400", String(describing: errorNode.input["httpResponseCode"]!)) + XCTAssertEqual("password: User password did not satisfy password policy requirements", errorNode.message) + + // Make sure that we are still at the registration form + XCTAssertEqual("Registration Form", continueNode.name) + } + + // TestRailCase(21273) + func testUserRegistrationFailureInvalidVerificationCode() async throws { + var node = await daVinci.start() + XCTAssertTrue(node is ContinueNode) + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Click the registration link + (continueNode.collectors[3] as? FlowCollector)?.value = "register" + node = await continueNode.next() + XCTAssertTrue(node is ContinueNode) + continueNode = node as! ContinueNode + + // Make sure that we are at the registration form + XCTAssertEqual("Registration Form", continueNode.name) + + let date = Date() + let formatter = DateFormatter() + formatter.dateFormat = "HHmmssSSSS" + + let newUser = "e2e" + formatter.string(from: date) + "@example.com" + + // Fill the registration form + (continueNode.collectors[0] as? TextCollector)?.value = newUser + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? TextCollector)?.value = userFname + (continueNode.collectors[3] as? TextCollector)?.value = userLname + (continueNode.collectors[4] as? SubmitCollector)?.value = "Save" + + // Click continue + (continueNode.collectors[4] as! SubmitCollector).value = "Save" + + node = await continueNode.next() + continueNode = node as! ContinueNode + + // Make sure that we are at the "Verification Code" screen + XCTAssertEqual("Enter verification code", continueNode.name) + + // Fill in the verification code and submit + (continueNode.collectors[0] as? TextCollector)?.value = "invalid" + (continueNode.collectors[1] as? SubmitCollector)?.value = "Verify" + node = await continueNode.next() + let errorNode = node as! ErrorNode + + // Make sure we get the expected error + XCTAssertEqual("400", String(describing: errorNode.input["code"]!)) + XCTAssertEqual("Invalid verification code", errorNode.message) + + // Make sure that we are still at verification code page + XCTAssertEqual("Enter verification code", continueNode.name) + + let daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + try await deleteUser(userName: newUser, pass: password) + } + + // TestRailCase(21277) + func testPasswordRecovery() async throws { + let date = Date() + let formatter = DateFormatter() + formatter.dateFormat = "HHmmssSSSS" + let newUser = "e2e" + formatter.string(from: date) + "@example.com" + + // Register a test user + try await registerUser(userName: newUser, password: password) + + // Launch DaVinci... + var node = await daVinci.start() + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Click on the "Having trouble..." link + (continueNode.collectors[4] as? FlowCollector)?.value = "click" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // At the "User Identifier Form" screen... + XCTAssertTrue(continueNode.collectors.count == 3) + XCTAssertEqual("User Identifier Form", continueNode.name) + XCTAssertEqual("Prompt For Email To Send Instructions To Reset Password", continueNode.description) + + XCTAssertTrue(continueNode.collectors[0] is TextCollector) // Username + XCTAssertTrue(continueNode.collectors[1] is SubmitCollector) // Continue + XCTAssertTrue(continueNode.collectors[2] is FlowCollector) // Back (link) + + XCTAssertEqual("Username", (continueNode.collectors[0] as! TextCollector).label) + XCTAssertEqual("Continue", (continueNode.collectors[1] as! SubmitCollector).label) + XCTAssertEqual("Back", (continueNode.collectors[2] as! FlowCollector).label) + + // Fill in the username and submit + (continueNode.collectors[0] as? TextCollector)?.value = newUser + (continueNode.collectors[1] as? SubmitCollector)?.value = "Submit" + + node = await continueNode.next() + continueNode = node as! ContinueNode + + // At the "Password Recovery Form" screen... + XCTAssertTrue(continueNode.collectors.count == 6) + XCTAssertEqual("Password Recovery Form", continueNode.name) + XCTAssertEqual("Enter The Recovery Code and Set New Password (Hint: Recovery code is 1234)", continueNode.description) + + XCTAssertTrue(continueNode.collectors[0] is TextCollector) // Recovery Code + XCTAssertTrue(continueNode.collectors[1] is PasswordCollector) // New Password + XCTAssertTrue(continueNode.collectors[2] is PasswordCollector) // Verify New Password + XCTAssertTrue(continueNode.collectors[3] is SubmitCollector) // Continue button + XCTAssertTrue(continueNode.collectors[4] is FlowCollector) // Resend recovery code + XCTAssertTrue(continueNode.collectors[5] is FlowCollector) // Cancel link + + XCTAssertEqual("Recovery Code", (continueNode.collectors[0] as! TextCollector).label) + XCTAssertEqual("New Password", (continueNode.collectors[1] as! PasswordCollector).label) + XCTAssertEqual("Verify New Password", (continueNode.collectors[2] as! PasswordCollector).label) + XCTAssertEqual("Continue", (continueNode.collectors[3] as! SubmitCollector).label) + XCTAssertEqual("Resend recovery code", (continueNode.collectors[4] as! FlowCollector).label) + XCTAssertEqual("Cancel", (continueNode.collectors[5] as! FlowCollector).label) + + // Fill in the recovery code and new password and submit + (continueNode.collectors[0] as? TextCollector)?.value = verificationCode + (continueNode.collectors[1] as? PasswordCollector)?.value = newPassword + (continueNode.collectors[2] as? PasswordCollector)?.value = newPassword + (continueNode.collectors[3] as? SubmitCollector)?.value = "Submit" + + node = await continueNode.next() + continueNode = node as! ContinueNode + + // At the "Successful password reset" screen... + XCTAssertTrue(continueNode.collectors.count == 1) + XCTAssertEqual("Password Reset Success", continueNode.name) + XCTAssertEqual("Success Message With Animated Checkmark", continueNode.description) + XCTAssertEqual("Continue", (continueNode.collectors[0] as! SubmitCollector).label) + + // Click "Continue" to finish the password reset process + (continueNode.collectors[0] as? SubmitCollector)?.value = "Continue" + + node = await continueNode.next() + let successNode = node as! SuccessNode + + let user = successNode.user + let userToken = await user?.token() + switch userToken! { + case .success(let token): + XCTAssertNotNil(token.accessToken) + break + case .failure(_): + XCTFail("Should have succeeded") + } + + let u = await daVinci.user() + await u?.logout() ?? { XCTFail("User is null") }() + + // After logout make sure the user is null + let daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + + // Delete the user from PingOne + try await deleteUser(userName: newUser, pass: newPassword) + } + + // TestRailCase(21278) + func testPasswordReset() async throws { + let date = Date() + let formatter = DateFormatter() + formatter.dateFormat = "HHmmssSSSS" + let newUser = "e2e" + formatter.string(from: date) + "@example.com" + + // Register a test user + try await registerUser(userName: newUser, password: password) + + // Launch DaVinci... + var node = await daVinci.start() + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Login + (continueNode.collectors[0] as? TextCollector)?.value = newUser + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // At the "Successful Login" page + XCTAssertEqual("Successful login", continueNode.name) + + // Click the "Reset password" link + (continueNode.collectors[1] as? FlowCollector)?.value = "click" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // At the "Change Password Form" screen... + XCTAssertTrue(continueNode.collectors.count == 5) + XCTAssertEqual("Change Password Form", continueNode.name) + XCTAssertEqual("Prompt for existing and new password", continueNode.description) + + XCTAssertTrue(continueNode.collectors[0] is PasswordCollector) // Current Password + XCTAssertTrue(continueNode.collectors[1] is PasswordCollector) // New Password + XCTAssertTrue(continueNode.collectors[2] is PasswordCollector) // Verify New Password + XCTAssertTrue(continueNode.collectors[3] is SubmitCollector) // Continue button + XCTAssertTrue(continueNode.collectors[4] is FlowCollector) // Cancel (link) + + XCTAssertEqual("Current Password", (continueNode.collectors[0] as! PasswordCollector).label) + XCTAssertEqual("New Password", (continueNode.collectors[1] as! PasswordCollector).label) + XCTAssertEqual("Verify New Password", (continueNode.collectors[2] as! PasswordCollector).label) + XCTAssertEqual("Continue", (continueNode.collectors[3] as! SubmitCollector).label) + XCTAssertEqual("Cancel", (continueNode.collectors[4] as! FlowCollector).label) + + // Fill in the reset password form and submit + (continueNode.collectors[0] as? PasswordCollector)?.value = password + (continueNode.collectors[1] as? PasswordCollector)?.value = newPassword + (continueNode.collectors[2] as? PasswordCollector)?.value = newPassword + (continueNode.collectors[3] as? SubmitCollector)?.value = "Submit" + + node = await continueNode.next() + continueNode = node as! ContinueNode + + // At the "Password Reset Success" screen... + XCTAssertTrue(continueNode.collectors.count == 1) + XCTAssertEqual("Password Reset Success", continueNode.name) + XCTAssertEqual("Success Message With Animated Checkmark", continueNode.description) + XCTAssertTrue(continueNode.collectors[0] is SubmitCollector) // Continue button + + // Click "Continue" to finish the password reset process + (continueNode.collectors[0] as? SubmitCollector)?.value = "Continue" + + node = await continueNode.next() + let successNode = node as! SuccessNode + + let user = successNode.user + let userToken = await user?.token() + switch userToken! { + case .success(let token): + XCTAssertNotNil(token.accessToken) + break + case .failure(_): + XCTFail("Should have succeeded") + } + + let u = await daVinci.user() + await u?.logout() ?? { XCTFail("User is null") }() + + // After logout make sure the user is null + let daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + + // Delete the user from PingOne + try await deleteUser(userName: newUser, pass: newPassword) + } + + // TestRailCase(24629) + func testAccountLocked() async throws { + let date = Date() + let formatter = DateFormatter() + formatter.dateFormat = "HHmmssSSSS" + let newUser = "e2e" + formatter.string(from: date) + "@example.com" + + // Register a test user + try await registerUser(userName: newUser, password: password) + + // Launch DaVinci... + var node = await daVinci.start() + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Fill in the login form with invalid credentials and submit... + // The following rules apply for the E2E test population: + // Account Lockout Rules: + // The user's account will be locked out after 2 distinct failed password attempts; + // repeated attempts of the same password are not counted. + // Automatically unlock accounts that were locked by failed password attempts after 1 second + (continueNode.collectors[0] as? TextCollector)?.value = newUser + (continueNode.collectors[1] as? PasswordCollector)?.value = "wrong1" + (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" + node = await continueNode.next() + let errorNode = node as! ErrorNode + + // Make sure we get the expected error + XCTAssertEqual("Invalid username and/or password", errorNode.message) + + // Make sure that we are still at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Enter invalid credentials again (this should lock the account) + (continueNode.collectors[0] as? TextCollector)?.value = newUser + (continueNode.collectors[1] as? PasswordCollector)?.value = "wrong2" + (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // Make sure that we are at the "Account Locked" screen... + XCTAssertTrue(continueNode.collectors.count == 1) + XCTAssertEqual("Account Locked Message", continueNode.name) + XCTAssertEqual("Notify when account will unlock", continueNode.description) + XCTAssertTrue(continueNode.collectors[0] is FlowCollector) // Back to sign on (link) + XCTAssertEqual("Back to sign on", (continueNode.collectors[0] as! FlowCollector).label) + + // Click "back" to return to the login page + (continueNode.collectors[0] as? SubmitCollector)?.value = "Back" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // Ensure that we are now at the login page and user is not logged in + XCTAssertEqual("E2E Login Form", continueNode.name) + var daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + + // Wait for a second so that the account gets unlocked + try await Task.sleep(nanoseconds: 1 * 1_500_000_000) + + // Enter the valid username and password of the account that was locked + (continueNode.collectors[0] as? TextCollector)?.value = newUser + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" // This will submit the form... + node = await continueNode.next() + XCTAssertTrue(node is ContinueNode) + continueNode = node as! ContinueNode + + // Verify that the login was successful + XCTAssertEqual("Successful login", continueNode.name) + + // Click continue + (continueNode.collectors[0] as! SubmitCollector).value = "Continue" + node = await continueNode.next() + XCTAssertTrue(node is SuccessNode) + let successNode = node as! SuccessNode + + let user = successNode.user + let userToken = await user?.token() + switch userToken! { + case .success(let token): + XCTAssertNotNil(token.accessToken) + break + case .failure(_): + XCTFail("Should have succeeded") + } + + let u = await daVinci.user() + await u?.logout() ?? { XCTFail("User is null") }() + + // After logout make sure the user is null + daVinciUser = await daVinci.user() + XCTAssertNil(daVinciUser) + + // Delete the test user + try await deleteUser(userName: newUser, pass: password) + + } + + /// Helper function to register a user + private func registerUser(userName: String, password: String) async throws { + var node = await daVinci.start() + var continueNode = node as! ContinueNode + + // Make sure that we are at the login form + XCTAssertEqual("E2E Login Form", continueNode.name) + + // Click the registration link + (continueNode.collectors[3] as? FlowCollector)?.value = "register" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // Make sure that we are at the registration form + XCTAssertEqual("Registration Form", continueNode.name) + + // Fill in the registration form + (continueNode.collectors[0] as? TextCollector)?.value = userName + (continueNode.collectors[1] as? PasswordCollector)?.value = password + (continueNode.collectors[2] as? TextCollector)?.value = userFname + (continueNode.collectors[3] as? TextCollector)?.value = userLname + (continueNode.collectors[4] as? SubmitCollector)?.value = "Save" + + // Click continue + (continueNode.collectors[4] as! SubmitCollector).value = "Save" + + node = await continueNode.next() + continueNode = node as! ContinueNode + + // User should be navigated to the verification code screen + XCTAssertEqual("Enter verification code", continueNode.name) + + // Fill in the verification code and submit + (continueNode.collectors[0] as? TextCollector)?.value = verificationCode + (continueNode.collectors[1] as? SubmitCollector)?.value = "Verify" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // User should be navigated to the "Successful user creation" screen... + XCTAssertEqual("Registration Complete", continueNode.name) + + // Click "Continue" to finish the registration process + (continueNode.collectors[0] as? SubmitCollector)?.value = "Continue" + node = await continueNode.next() + XCTAssertTrue(node is SuccessNode) + + // logout the user + let u = await daVinci.user() + await u?.logout() ?? { XCTFail("User is null") }() + } + + /// Helper function to delete a user + private func deleteUser(userName: String, pass: String) async throws { + var node = await daVinci.start() + var continueNode = node as! ContinueNode + + // // Make sure that we are at the Login form... + XCTAssertEqual("E2E Login Form", continueNode.name) + + (continueNode.collectors[0] as? TextCollector)?.value = userName + (continueNode.collectors[1] as? PasswordCollector)?.value = pass + (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" + node = await continueNode.next() + XCTAssertTrue(node is ContinueNode) + continueNode = node as! ContinueNode + + // Verify the Successful login form + XCTAssertEqual("Successful login", continueNode.name) + XCTAssertEqual("Successfully logged in to DaVinci", continueNode.description) + XCTAssertEqual("Continue", (continueNode.collectors[0] as! SubmitCollector).label) + XCTAssertEqual("Reset password...", (continueNode.collectors[1] as! FlowCollector).label) + XCTAssertEqual("Delete user...", (continueNode.collectors[2] as! FlowCollector).label) + + // Click the "Delete user" link + (continueNode.collectors[2] as? FlowCollector)?.value = "Delete User" + node = await continueNode.next() + continueNode = node as! ContinueNode + + // Validate success user deletion screen + XCTAssertEqual("Success", continueNode.name) + XCTAssertEqual("User has been successfully deleted", continueNode.description) + } +} diff --git a/Davinci/DavinciTests/mock/MockAPIEndpoint.swift b/Davinci/DavinciTests/mock/MockAPIEndpoint.swift new file mode 100644 index 0000000..ffc3f93 --- /dev/null +++ b/Davinci/DavinciTests/mock/MockAPIEndpoint.swift @@ -0,0 +1,43 @@ +// +// MockAPIEndpoint.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +enum MockAPIEndpoint { + static let baseURL = "https://auth.test-one-pingone.com" + + case authorization + case token + case userinfo + case endSession + case revocation + case discovery + case customHTMLTemplate + + var url: URL { + switch self { + case .authorization: + return URL(string: "\(MockAPIEndpoint.baseURL)/authorize")! + case .token: + return URL(string: "\(MockAPIEndpoint.baseURL)/token")! + case .userinfo: + return URL(string: "\(MockAPIEndpoint.baseURL)/userinfo")! + case .endSession: + return URL(string: "\(MockAPIEndpoint.baseURL)/signoff")! + case .revocation: + return URL(string: "\(MockAPIEndpoint.baseURL)/revoke")! + case .discovery: + return URL(string: "\(MockAPIEndpoint.baseURL)/.well-known/openid-configuration")! + case .customHTMLTemplate: + return URL(string: "\(MockAPIEndpoint.baseURL)/customHTMLTemplate")! + } + } +} diff --git a/Davinci/DavinciTests/mock/MockResponse.swift b/Davinci/DavinciTests/mock/MockResponse.swift new file mode 100644 index 0000000..246efd1 --- /dev/null +++ b/Davinci/DavinciTests/mock/MockResponse.swift @@ -0,0 +1,222 @@ +// +// MockResponse.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +struct MockResponse { + static let headers = ["Content-Type": "application/json"] + + // Freturn the OpenID configuration response as Data + static var openIdConfigurationResponse: Data { + return """ + { + "authorization_endpoint" : "http://auth.test-one-pingone.com/authorize", + "token_endpoint" : "https://auth.test-one-pingone.com/token", + "userinfo_endpoint" : "https://auth.test-one-pingone.com/userinfo", + "end_session_endpoint" : "https://auth.test-one-pingone.com/signoff", + "revocation_endpoint" : "https://auth.test-one-pingone.com/revoke" + } + """.data(using: .utf8)! + } + + // return the token response as Data + static var tokenResponse: Data { + return """ + { + "access_token" : "Dummy AccessToken", + "token_type" : "Dummy Token Type", + "scope" : "openid email address", + "refresh_token" : "Dummy RefreshToken", + "expires_in" : 1, + "id_token" : "Dummy IdToken" + } + """.data(using: .utf8)! + } + + // return the userinfo response as Data + static var userinfoResponse: Data { + return """ + { + "sub" : "test-sub", + "name" : "test-name", + "email" : "test-email", + "phone_number" : "test-phone_number", + "address" : "test-address" + } + """.data(using: .utf8)! + } + + // return an empty revoke response as Data + static var revokeResponse: Data{ + return Data() + } + + // Headers for the authorize response + static let authorizeResponseHeaders: [String: String] = + [ + "Content-Type": "application/json; charset=utf-8", + "Set-Cookie": """ + interactionId=038e8128-272a-4a15-b97b-379aa1447149; Max-Age=3600; Path=/; Expires=Wed, 27 Mar 9999 05:06:30 GMT; HttpOnly + """, +// "Set-Cookie": """ +// interactionToken=71c65504463355679fd247900441c36afb6be6c00d45aa169500b7cd753894d46d68feb4952ff0843ff4b287220a66cb3d58a3bc41e71724f111b034d0458aac8a5153859ed96825ef8c6a6400e7ae9de82a7353fc3c9886ba835853db8c0957ea4cd0a52d20d4fb50b4419dc9df33a53889f52abeb04f517b6c7c8efb0b58f0; Max-Age=3600; Path=/; Expires=Wed, 27 Mar 9999 05:06:30 GMT; HttpOnly +// """, +// "Set-Cookie": """ +// skProxyApiEnvironmentId=us-west-2; Max-Age=900; Path=/; Expires=Wed, 27 Mar 9999 04:21:30 GMT; HttpOnly +// """ + ] + + // return the authorize response as Data + static var authorizeResponse: Data { + return """ + { + "_links": { + "next": { + "href": "http://auth.test-one-pingone.com/customHTMLTemplate" + } + }, + "interactionId": "008bccea-914b-49da-b2a1-5cd3f83f4372", + "interactionToken": "2a0d9bcdbdeb5ea14ef34d680afc45f37a56e190e306a778f01d768b271bf1e976aaf4154b633381e1299b684d3a4a66d3e1c6d419a7d20657bf4f32c741d78f67d41e08eb0e5f1070edf780809b4ccea8830866bcedb388d8f5de13e89454d353bcca86d4dcd5d7872efc929f7e5199d8d127d1b2b45499c42856ce785d8664", + "eventName": "continue", + "isResponseCompatibleWithMobileAndWebSdks": true, + "id": "cq77vwelou", + "companyId": "0c6851ed-0f12-4c9a-a174-9b1bf8b438ae", + "flowId": "ebac77c8fbf68d3dac68c5dd804a936f", + "connectionId": "867ed4363b2bc21c860085ad2baa817d", + "capabilityName": "customHTMLTemplate", + "formData": { + "value": { + "username": "", + "password": "" + } + }, + "form": { + "name": "Username/Password Form", + "description": "Test Description", + "category": "CUSTOM_HTML", + "components": { + "fields": [ + { + "type": "TEXT", + "key": "username", + "label": "Username" + }, + { + "type": "PASSWORD", + "key": "password", + "label": "Password" + }, + { + "type": "SUBMIT_BUTTON", + "key": "SIGNON", + "label": "Sign On" + }, + { + "type": "FLOW_BUTTON", + "key": "TROUBLE", + "label": "Having trouble signing on?" + }, + { + "type": "FLOW_BUTTON", + "key": "REGISTER", + "label": "No account? Register now!" + } + ] + } + } + } + """.data(using: .utf8)! + } + + // Headers for the custom HTML template response + static let customHTMLTemplateHeaders: [String: String] = [ + "Content-Type": "application/json; charset=utf-8", + "Set-Cookie": """ + ST=session_token; Max-Age=3600; Path=/; Expires=Wed, 27 Mar 9999 05:06:30 GMT; HttpOnly + """ + ] + + // return the custom HTML template response as Data + static var customHTMLTemplate: Data { + return """ + { + "interactionId": "033e1338-c271-4dd7-8d74-fc2eacc135d8", + "companyId": "94e3268d-847d-47aa-a45e-1ef8dd8f4df0", + "connectionId": "26146c8065741406afb0899484e361a7", + "connectorId": "pingOneAuthenticationConnector", + "id": "5dtrjnrwox", + "capabilityName": "returnSuccessResponseRedirect", + "environment": { + "id": "94e3268d-847d-47aa-a45e-1ef8dd8f4df0" + }, + "session": { + "id": "d0598645-c2f7-4b94-adc9-401a896eaffb" + }, + "status": "COMPLETED", + "authorizeResponse": { + "code": "03dbd5a2-db72-437c-8728-fc33b860083c" + }, + "success": true, + "interactionToken": "5ad09feac8982d668c5f07d1eaf544bdf2309247146999c0139f7ebb955c24743b97a01e3bf67360121cd85d7a9e1d966c3f4b7e27f21206a5304d305951864cc34a37900f3326f8000c7bc731af9ba78a681eb14d4bf767172e8a7149e4df3e054b4245bdea5612e9ec0c0d8cb349b55dcf10db30de075dfc79f6c765046d99" + } + """.data(using: .utf8)! + } + + // return the custom HTML template response with invalid password as Data + static var customHTMLTemplateWithInvalidPassword: Data { + return """ + { + "interactionId": "00444ecd-0901-4b57-acc3-e1245971205b", + "companyId": "0c6851ed-0f12-4c9a-a174-9b1bf8b438ae", + "connectionId": "94141bf2f1b9b59a5f5365ff135e02bb", + "connectorId": "pingOneSSOConnector", + "id": "dnu7jt3sjz", + "capabilityName": "checkPassword", + "errorCategory": "NotSet", + "code": "Invalid username and/or password", + "cause": null, + "expected": true, + "message": "Invalid username and/or password", + "httpResponseCode": 400, + "details": [ + { + "rawResponse": { + "id": "b187c1c7-e9fe-4f72-a554-1b2876babafe", + "code": "INVALID_DATA", + "message": "The request could not be completed. One or more validation errors were in the request.", + "details": [ + { + "code": "INVALID_VALUE", + "target": "password", + "message": "The provided password did not match provisioned password", + "innerError": { + "failuresRemaining": 4 + } + } + ] + }, + "statusCode": 400 + } + ], + "isResponseCompatibleWithMobileAndWebSdks": true, + "correlationId": "b187c1c7-e9fe-4f72-a554-1b2876babafe" + } + """.data(using: .utf8)! + } + + static var tokenErrorResponse: Data { + return """ + { + "error": "Invalid Grant" + } + """.data(using: .utf8)! + } +} diff --git a/Davinci/DavinciTests/mock/MockURLProtocol.swift b/Davinci/DavinciTests/mock/MockURLProtocol.swift new file mode 100644 index 0000000..a62720a --- /dev/null +++ b/Davinci/DavinciTests/mock/MockURLProtocol.swift @@ -0,0 +1,57 @@ +// +// MockURLProtocol.swift +// DavinciTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest + +class MockURLProtocol: URLProtocol { + public static var requestHistory: [URLRequest] = [URLRequest]() + + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + static func startInterceptingRequests() { + URLProtocol.registerClass(MockURLProtocol.self) + } + + static func stopInterceptingRequests() { + URLProtocol.unregisterClass(MockURLProtocol.self) + requestHistory.removeAll() + } + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + MockURLProtocol.requestHistory.append(request) + + guard let handler = MockURLProtocol.requestHandler else { + XCTFail("Received unexpected request with no handler set") + return + } + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() { + + } +} diff --git a/Davinci/README.md b/Davinci/README.md new file mode 100644 index 0000000..546cfeb --- /dev/null +++ b/Davinci/README.md @@ -0,0 +1,210 @@ +

+ + Logo + +


+

+ +# PingDavinci + +## Overview + +PingDavinci is a powerful and flexible library for Authentication and Authorization. It is designed to be easy to use and +extensible. It provides a simple API for navigating the authentication flow and handling the various states that can +occur during the authentication process. + + + +## Integrating the SDK into your project + +Use Cocoapods or Swift Package Manger + +## Usage + +To use the `DaVinci` class, you need to create an instance of it by passing a configuration block to the `createDaVinci` method. The +configuration block allows you to customize various aspects of the `DaVinci` instance, such as the timeout and logging. + +Here's an example of how to create a `DaVinci` instance: + +```swift +let daVinci = DaVinci.createDaVinci { config in + // Oidc as module + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "test" + oidcValue.discoveryEndpoint = "https://auth.test-one-pingone.com/0c6851ed-0f12-4c9a-a174-9b1bf8b438ae/as/.well-known/openid-configuration" + oidcValue.scopes = ["openid", "email", "address"] + oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" + } + } +var node = await daVinci.start() +node = await (node as! ContinueNode).next() +``` + +The `PingDavinci` depends on `PingOidc` module. It discovers the OIDC endpoints with `discoveryEndpoint` attribute. + +The `start` method returns a `Node` instance. The `Node` class represents the current state of the application. You can +use the `next` method to transition to the next state. + +## More DaVinci Configuration +```swift +let daVinci = DaVinci.createDaVinci { config in + config.timeout = 30 + config.logger = LogManager.standard + config.module(OidcModule.config) { oidcValue in + //... + oidcValue.storage = MemoryStorage() + } +} +``` + + +### Navigate the authentication Flow + +```swift +let node = await daVinci.start() //Start the flow + +//Determine the Node Type +switch (node) { +case is ContinueNode: do {} +case is ErrorNode: do {} +case is FailureNode: do {} +case is SuccessNode: do {} + } +``` + +| Node Type | Description | +|------------|:----------------------------------------------------------------------------------------------------------| +| ContinueNode | In the middle of the flow, call ```node.next``` to move to next Node in the flow | +| FailureNode | Unexpected Error, e.g Network, parsing ```node.cause``` to retrieve the cause of the error | +| ErrorNode| Bad Request from the server, e.g Invalid Password, OTP, username ```node.message``` for the error message | +| SuccessNode| Authentication successful ```node.session``` to retrieve the session | + +### Provide input +For `ContinueNode` Node, you can access list of Collector with `node.collectors` and provide input to +the `Collector`. +Currently, there are, `TextCollector`, `PasswordCollector`, `SubmitCollector`, `FlowCollector`, but more will be added in the future, such as `Fido`, +`SocialLoginCollector`, etc... + +To access the collectors, you can use the following code: +```swift +node.collectors.forEach { item in + switch(item) { + case is TextCollector: + (item as! TextCollector).value = "My First Name" + case is PasswordCollector: + (item as! PasswordCollector).value = "My Password" + case is SubmitCollector: + (item as! SubmitCollector).value = "click me" + case is FlowCollector: + (item as! FlowCollector).value = "Forgot Password" + } +} + +//Move to next Node, and repeat the flow until it reaches `SuccessNode` or `ErrorNode` Node +let next = node.next() +``` + +### Error Handling + +For `FailureNode` Node, you can retrieve the cause of the error by using `node.cause`. The `cause` is an `Error` instance, +when receiving an error, you cannot continue the Flow, you may want to display a generic message to the user, and report +the issue to the Support team. +The Error may include Network issue, parsing issue, API Error (Server response other that 2xx and 400) and other unexpected issues. + +For `ErrorNode` Node, you can retrieve the error message by using `node.message`. The `message` is a `String` object, +when receiving a failure, you can continue the Flow with previous `ContinueNode` Node, but you may want to display the error message to the user. +e.g "Username/Password is incorrect", "OTP is invalid", etc... +```swift +let node = await daVinci.start() //Start the flow + +//Determine the Node Type +switch (node) { +case is ContinueNode: do {} +case is FailureNode: + (node as! FailureNode).cause //Retrieve the cause of the Failure +case is ErrorNode: + (node as! ErrorNode).message //Retrieve the error message +case is SuccessNode: do {} +} +``` + +### Node Identifier +You can use the `node.id` to identify the current state of the flow. The `id` is a unique identifier for each node. + +For example, you can use the `id` to determine if the current state is `Forgot Passowrd`, `Registration`, etc.... + +```swift + +var state = "" +switch (node.id) { +case "cq77vwelou": state = "Sign On" +case "qwnvng32z3": state = "Password Reset" +case "4dth5sn269": state = "Create Your Profile" +case "qojn9nsdxh": state = "Verification Code" +case "fkekf3oi8e": state = "Enter New Password" +default: state = "" +} +``` + +Other than `id`, you can also use `node.name` to retrieve the name of the Node, `node.description` to retrieve the description of the Node. + + +### Work with SwiftUI + +ViewModel +```swift +//Define State that listen by the View + +@Published var state: Node = EmptyNode() + +//Start the DaVinci flow +let next = await daVinci.start() + +//Update the state +state = next + +func next(node: ContinueNode) { + val next = await node.next() + state = next + +} +``` + +View +```swift +if let node = state.node { + switch node { + case is ContinueNode: + // Handle ContinueNode case + break + case is ErrorNode: + // Handle Error case + break + case is FailureNode: + // Handle Failure case + break + case is SuccessNode: + // Handle Success case + break + default: + break + } +} +``` + +### Post Authentication +After authenticate with DaVinci, the user session will be stored in the storage. +To retrieve the existing session, you can use the following code: + +```swift +//Retrieve the existing user, if token exists in the storage, ```user``` will be not nil. +//However, even with the user object, you may not be able to retrieve a valid token, as the token and refresh token may be expired. + +let user: User? = await daVinci.user() + +_ = await user?.token() +await user?.revoke() +_ = await user?.userinfo(cache: false) +await user?.logout() + +``` diff --git a/Davinci/images/davinciSequence.png b/Davinci/images/davinciSequence.png new file mode 100644 index 0000000..916cad4 Binary files /dev/null and b/Davinci/images/davinciSequence.png differ diff --git a/LICENSE b/LICENSE index 2e96d7b..dd424e1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 ForgeRock Community +Copyright (c) 2024 Ping Idenitty Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Logger/Logger.xcodeproj/project.pbxproj b/Logger/Logger.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e7e0a8e --- /dev/null +++ b/Logger/Logger.xcodeproj/project.pbxproj @@ -0,0 +1,500 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A5A712402CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A7123F2CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy */; }; + A5A796B72BE04E68004D0F2D /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5A796AE2BE04E67004D0F2D /* PingLogger.framework */; }; + A5A796BC2BE04E68004D0F2D /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A796BB2BE04E68004D0F2D /* LoggerTests.swift */; }; + A5A796BD2BE04E68004D0F2D /* Logger.h in Headers */ = {isa = PBXBuildFile; fileRef = A5A796B12BE04E67004D0F2D /* Logger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A5A796D02BE04EDE004D0F2D /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A796CF2BE04EDE004D0F2D /* Logger.swift */; }; + A5A796D22BE050DB004D0F2D /* NoneLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A796D12BE050DB004D0F2D /* NoneLogger.swift */; }; + A5A796D62BE05C9B004D0F2D /* StandardLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A796D52BE05C9B004D0F2D /* StandardLogger.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + A5A796B82BE04E68004D0F2D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A5A796A52BE04E67004D0F2D /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5A796AD2BE04E67004D0F2D; + remoteInfo = PingLogger; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + A5A7123F2CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + A5A796AE2BE04E67004D0F2D /* PingLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A5A796B12BE04E67004D0F2D /* Logger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Logger.h; sourceTree = ""; }; + A5A796B62BE04E68004D0F2D /* LoggerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoggerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A5A796BB2BE04E68004D0F2D /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; + A5A796CF2BE04EDE004D0F2D /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + A5A796D12BE050DB004D0F2D /* NoneLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoneLogger.swift; sourceTree = ""; }; + A5A796D52BE05C9B004D0F2D /* StandardLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardLogger.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A5A796AB2BE04E67004D0F2D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A5A796B32BE04E68004D0F2D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A796B72BE04E68004D0F2D /* PingLogger.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A5A796A42BE04E67004D0F2D = { + isa = PBXGroup; + children = ( + A5A796B02BE04E67004D0F2D /* Logger */, + A5A796BA2BE04E68004D0F2D /* LoggerTests */, + A5A796AF2BE04E67004D0F2D /* Products */, + ); + sourceTree = ""; + }; + A5A796AF2BE04E67004D0F2D /* Products */ = { + isa = PBXGroup; + children = ( + A5A796AE2BE04E67004D0F2D /* PingLogger.framework */, + A5A796B62BE04E68004D0F2D /* LoggerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + A5A796B02BE04E67004D0F2D /* Logger */ = { + isa = PBXGroup; + children = ( + A5A7123F2CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy */, + A5A796B12BE04E67004D0F2D /* Logger.h */, + A5A796CF2BE04EDE004D0F2D /* Logger.swift */, + A5A796D12BE050DB004D0F2D /* NoneLogger.swift */, + A5A796D52BE05C9B004D0F2D /* StandardLogger.swift */, + ); + path = Logger; + sourceTree = ""; + }; + A5A796BA2BE04E68004D0F2D /* LoggerTests */ = { + isa = PBXGroup; + children = ( + A5A796BB2BE04E68004D0F2D /* LoggerTests.swift */, + ); + path = LoggerTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A5A796A92BE04E67004D0F2D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A796BD2BE04E68004D0F2D /* Logger.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A5A796AD2BE04E67004D0F2D /* PingLogger */ = { + isa = PBXNativeTarget; + buildConfigurationList = A5A796C02BE04E68004D0F2D /* Build configuration list for PBXNativeTarget "PingLogger" */; + buildPhases = ( + A5A796A92BE04E67004D0F2D /* Headers */, + A5A796AA2BE04E67004D0F2D /* Sources */, + A5A796AB2BE04E67004D0F2D /* Frameworks */, + A5A796AC2BE04E67004D0F2D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PingLogger; + productName = PingLogger; + productReference = A5A796AE2BE04E67004D0F2D /* PingLogger.framework */; + productType = "com.apple.product-type.framework"; + }; + A5A796B52BE04E68004D0F2D /* LoggerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A5A796C32BE04E68004D0F2D /* Build configuration list for PBXNativeTarget "LoggerTests" */; + buildPhases = ( + A5A796B22BE04E68004D0F2D /* Sources */, + A5A796B32BE04E68004D0F2D /* Frameworks */, + A5A796B42BE04E68004D0F2D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A5A796B92BE04E68004D0F2D /* PBXTargetDependency */, + ); + name = LoggerTests; + productName = PingLoggerTests; + productReference = A5A796B62BE04E68004D0F2D /* LoggerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A5A796A52BE04E67004D0F2D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + A5A796AD2BE04E67004D0F2D = { + CreatedOnToolsVersion = 15.3; + LastSwiftMigration = 1530; + }; + A5A796B52BE04E68004D0F2D = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = A5A796A82BE04E67004D0F2D /* Build configuration list for PBXProject "Logger" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A5A796A42BE04E67004D0F2D; + productRefGroup = A5A796AF2BE04E67004D0F2D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A5A796AD2BE04E67004D0F2D /* PingLogger */, + A5A796B52BE04E68004D0F2D /* LoggerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A5A796AC2BE04E67004D0F2D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A712402CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A5A796B42BE04E68004D0F2D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A5A796AA2BE04E67004D0F2D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A796D62BE05C9B004D0F2D /* StandardLogger.swift in Sources */, + A5A796D02BE04EDE004D0F2D /* Logger.swift in Sources */, + A5A796D22BE050DB004D0F2D /* NoneLogger.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A5A796B22BE04E68004D0F2D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A796BC2BE04E68004D0F2D /* LoggerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + A5A796B92BE04E68004D0F2D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5A796AD2BE04E67004D0F2D /* PingLogger */; + targetProxy = A5A796B82BE04E68004D0F2D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + A5A796BE2BE04E68004D0F2D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + A5A796BF2BE04E68004D0F2D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + A5A796C12BE04E68004D0F2D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Logger; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A5A796C22BE04E68004D0F2D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Logger; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + A5A796C42BE04E68004D0F2D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.LoggerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Debug; + }; + A5A796C52BE04E68004D0F2D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.LoggerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A5A796A82BE04E67004D0F2D /* Build configuration list for PBXProject "Logger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5A796BE2BE04E68004D0F2D /* Debug */, + A5A796BF2BE04E68004D0F2D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A5A796C02BE04E68004D0F2D /* Build configuration list for PBXNativeTarget "PingLogger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5A796C12BE04E68004D0F2D /* Debug */, + A5A796C22BE04E68004D0F2D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A5A796C32BE04E68004D0F2D /* Build configuration list for PBXNativeTarget "LoggerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5A796C42BE04E68004D0F2D /* Debug */, + A5A796C52BE04E68004D0F2D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A5A796A52BE04E67004D0F2D /* Project object */; +} diff --git a/Logger/Logger.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Logger/Logger.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..16cf018 --- /dev/null +++ b/Logger/Logger.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,17 @@ + + + + + FILEHEADER + +// ___FILENAME___ +// ___PACKAGENAME___ +// +// Copyright (c) ___YEAR___ Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + + diff --git a/Logger/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme b/Logger/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme new file mode 100644 index 0000000..1e77331 --- /dev/null +++ b/Logger/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logger/Logger/Logger.h b/Logger/Logger/Logger.h new file mode 100644 index 0000000..38b776f --- /dev/null +++ b/Logger/Logger/Logger.h @@ -0,0 +1,21 @@ +// +// Logger.h +// Logger +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +#import + +//! Project version number for Logger. +FOUNDATION_EXPORT double LoggerVersionNumber; + +//! Project version string for Logger. +FOUNDATION_EXPORT const unsigned char LoggerVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Logger/Logger/Logger.swift b/Logger/Logger/Logger.swift new file mode 100644 index 0000000..079ea53 --- /dev/null +++ b/Logger/Logger/Logger.swift @@ -0,0 +1,46 @@ +// +// Logger.swift +// PingLogger +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Logger protocol that provides methods for logging different levels of information. +public protocol Logger { + /// Logs a debug message. + /// - Parameter message: The debug message to be logged. + func d(_ message: String) + + /// Logs an informational message. + /// - Parameter message: The message to be logged. + func i(_ message: String) + + /// Logs a warning message. + /// - Parameters: + /// - message: The warning message to be logged. + /// - error: Optional Error associated with the warning. + func w(_ message: String, error: Error?) + + /// Logs an error message. + /// - Parameters: + /// - message: The error message to be logged. + /// - error: Optional Error associated with the warning. + func e(_ message: String, error: Error?) +} + +/// LogManager to access the global logger instances +public struct LogManager { + private static var shared: Logger = NoneLogger() + + /// Global logger instance. If no logger is set, it defaults to `NoneLogger()`. + public static var logger: Logger { + get { shared } + set { shared = newValue } + } +} diff --git a/Logger/Logger/NoneLogger.swift b/Logger/Logger/NoneLogger.swift new file mode 100644 index 0000000..a1eacd5 --- /dev/null +++ b/Logger/Logger/NoneLogger.swift @@ -0,0 +1,41 @@ +// +// NoneLogger.swift +// PingLogger +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// The NoneLogger class is an implementation of the Logger interface that performs no operations. +/// This can be used as a default or placeholder logger. +public class NoneLogger: Logger { + /// Logs a debug message. + /// - Parameter message: The debug message to be logged. + public func d(_ message: String) {} + + /// Logs an informational message. + /// - Parameter message: The message to be logged. + public func i(_ message: String) {} + + /// Logs a warning message. + /// - Parameters: + /// - message: The warning message to be logged. + /// - error: Optional Error associated with the warning. + public func w(_ message: String, error: Error?) {} + + /// Logs an error message. + /// - Parameters: + /// - message: The error message to be logged. + /// - error: Optional Error associated with the warning. + public func e(_ message: String, error: Error?) {} +} + +extension LogManager { + /// Static logger of `NoneLogger` type + public static var none: Logger { return NoneLogger() } +} diff --git a/Logger/Logger/PrivacyInfo.xcprivacy b/Logger/Logger/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..fcfc9b9 --- /dev/null +++ b/Logger/Logger/PrivacyInfo.xcprivacy @@ -0,0 +1,10 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/Logger/Logger/StandardLogger.swift b/Logger/Logger/StandardLogger.swift new file mode 100644 index 0000000..ef87af1 --- /dev/null +++ b/Logger/Logger/StandardLogger.swift @@ -0,0 +1,77 @@ +// +// StandardLogger.swift +// PingLogger +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import os.log + +/// Stadard Logger to log to the iOS Console +public class StandardLogger: Logger { + /// SDK Version to be updated with each release + private let sdkVersion = "Ping SDK 1.0.0" + var log: OSLog + + /// Initializer for StandardLogger + /// - Parameter log: Optional OSLog. Default: `OSLog(subsystem: "com.pingidentity.ios", category: "Standard")` + public init (log: OSLog? = nil) { + self.log = log ?? OSLog(subsystem: "com.pingidentity.ios", category: "Standard") + } + + /// Logs a debug message. + /// - Parameter message: The debug message to be logged. + public func d(_ message: String) { + logMessage(message, log: log, type: .debug, error: nil) + } + + /// Logs an informational message. + /// - Parameter message: The message to be logged. + public func i(_ message: String) { + logMessage(message, log: log, type: .info, error: nil) + } + + /// Logs a warning message. + /// - Parameters: + /// - message: The warning message to be logged. + /// - error: Optional Error associated with the warning. + public func w(_ message: String, error: Error?) { + logMessage(message, log: log, type: .error, error: error) + } + + /// Logs an error message. + /// - Parameters: + /// - message: The error message to be logged. + /// - error: Optional Error associated with the warning. + public func e(_ message: String, error: Error?) { + logMessage(message, log: log, type: .fault, error: error) + } + + private func logMessage(_ message: String, log: OSLog = .default, type: OSLogType = .default, error: Error? = nil) { + let errorMessage = (error == nil ? "" : ", Error: \(error!.localizedDescription)") + os_log("%{public}@", log: log, type: type, "[\(sdkVersion)] \(message)\(errorMessage)") + } +} + +/// Warning Logger that only logs warnings and errors +public class WarningLogger: StandardLogger { + /// Logs a debug message. This implementation does nothing. + /// - Parameter message: The debug message to be logged. + public override func d(_ message: String) { } + + /// Logs an info message. This implementation does nothing. + /// - Parameter message: The informational message to be logged. + public override func i(_ message: String) { } +} + +extension LogManager { + /// Static logger of `StandardLogger` type + public static var standard: Logger { return StandardLogger() } + /// Static logger of `WarningLogger` type + public static var warning: Logger { return WarningLogger() } +} diff --git a/Logger/LoggerTests/LoggerTests.swift b/Logger/LoggerTests/LoggerTests.swift new file mode 100644 index 0000000..10f223d --- /dev/null +++ b/Logger/LoggerTests/LoggerTests.swift @@ -0,0 +1,86 @@ +// +// LoggerTests.swift +// LoggerTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingLogger + +final class LoggerTests: XCTestCase { + + // TestRailCase(22062, 22063, 22064, 22065) + func testLoggerSetAndGet() { + let noneLogger = NoneLogger() + LogManager.logger = noneLogger + XCTAssert(LogManager.logger is NoneLogger) + + let standardLogger = StandardLogger() + LogManager.logger = standardLogger + XCTAssert(LogManager.logger is StandardLogger) + + let warningLogger = WarningLogger() + LogManager.logger = warningLogger + XCTAssert(LogManager.logger is WarningLogger) + } + + + func testDefaultLoggers() { + let noneLogger = LogManager.none + XCTAssert(noneLogger is NoneLogger) + + let standardLogger = LogManager.standard + XCTAssert(standardLogger is StandardLogger) + + let warningLogger = LogManager.warning + XCTAssert(warningLogger is WarningLogger) + } + + + // TestRailCase(24702) + func testCustomLogger() { + var customLogger = LogManager.customLogger + XCTAssert(customLogger is CustomLogger) + + customLogger = CustomLogger() + LogManager.logger = customLogger + XCTAssert(LogManager.logger is CustomLogger) + } + +} + +struct CustomLogger: Logger { + + func i(_ message: String) { + } + + func d(_ message: String) { + } + + func w(_ message: String, error: Error?) { + if let error = error { + print("\(message): \(error)") + } else { + print(message) + } + } + + func e(_ message: String, error: Error?) { + if let error = error { + print("\(message): \(error)") + } else { + print(message) + } + } +} + +extension LogManager { + static var customLogger: Logger { + return CustomLogger() + } +} diff --git a/Logger/README.md b/Logger/README.md new file mode 100644 index 0000000..45844e5 --- /dev/null +++ b/Logger/README.md @@ -0,0 +1,104 @@ +

+ + Logo + +


+

+ +# PingLogger SDK + +The PingLogger SDK provides a versatile logging interface and a set of common loggers for the Ping +SDKs. + +## Integrating the SDK into your project + +Use Cocoapods or Swift Package Manger + +## How to Use the SDK + +### Logging to the iOS Console + +To log messages to the console, use the `standard` logger: + +```swift +import PingLogger + +let logger = LogManager.standard +logger.i("Hello World") +``` + +With the default the log will Tag with the SDK Version: +``` +Ping SDK +``` + +### Disabling Logging + +The PingLogger SDK provides a `none` logger that does not log any messages: + +```swift +import PingLogger + +let logger = LogManager.none +logger.i("Hello World") // This message will not be logged +``` + +### Creating a Custom Logger + +You can create a custom logger to suit your specific needs. For example, here's how to create a +logger that only logs +warning and error messages: + +```swift +struct WarningErrorOnlyLogger: Logger { + + func i(_ message: String) { + } + + func d(_ message: String) { + } + + func w(_ message: String, error: Error?) { + if let error = error { + print("\(message): \(error)") + } else { + print(message) + } + } + + func e(_ message: String, error: Error?) { + if let error = error { + print("\(message): \(error)") + } else { + print(message) + } + } +} + +extension LogManager { + static var warningErrorOnly: Logger { + return WarningErrorOnlyLogger() + } +} +``` + +To use your custom logger: + +```swift +let logger = LogManager.warningErrorOnly +logger.i("Hello World") // This message will not be logged +``` + +## Shared Logger + +LogManager also provides a global shared logger: `LogManager.logger`. Default value for the `LogManager.logger` is `none`, however any type conforming to `Logger` protocol can be assigned to it, inluding the `standard` and `warning` loggers and any custom logger. + +## Available Loggers + +The PingLogger SDK provides the following loggers: + +| Logger | Description | +|----------|-------------------------------------------------------| +| standard | Logs messages to iOS Console | +| warning | Logs warning and error messages to iOS Console | +| none | Disables logging | diff --git a/Oidc/Oidc.xcodeproj/project.pbxproj b/Oidc/Oidc.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3fd8dee --- /dev/null +++ b/Oidc/Oidc.xcodeproj/project.pbxproj @@ -0,0 +1,592 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 63; + objects = { + +/* Begin PBXBuildFile section */ + 3AB1CA102BD6F99C003FCE3C /* PingOidc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AB1CA072BD6F99C003FCE3C /* PingOidc.framework */; }; + 3AB1CA162BD6F99C003FCE3C /* Oidc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3AB1CA0A2BD6F99C003FCE3C /* Oidc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3AB1CA2D2BD7727B003FCE3C /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA2A2BD7727B003FCE3C /* Token.swift */; }; + 3AB1CA2E2BD7727B003FCE3C /* Pkce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA2B2BD7727B003FCE3C /* Pkce.swift */; }; + 3AB1CA312BD859BC003FCE3C /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA302BD859BC003FCE3C /* User.swift */; }; + 3AB1CA332BD8823C003FCE3C /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA322BD8823C003FCE3C /* Agent.swift */; }; + 3AB1CA352BD884E4003FCE3C /* OidcClientConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA342BD884E4003FCE3C /* OidcClientConfig.swift */; }; + 3AB1CA3B2BD96089003FCE3C /* OidcUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA3A2BD96089003FCE3C /* OidcUser.swift */; }; + 3AB1CA3D2BD961A2003FCE3C /* OidcClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA3C2BD961A2003FCE3C /* OidcClient.swift */; }; + A50966B62C4C342800A4E5B5 /* OidcError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966B52C4C342800A4E5B5 /* OidcError.swift */; }; + A50966B82C4C34CB00A4E5B5 /* AuthCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966B72C4C34CB00A4E5B5 /* AuthCode.swift */; }; + A50966BA2C4C3E7300A4E5B5 /* OpenIdConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966B92C4C3E7300A4E5B5 /* OpenIdConfiguration.swift */; }; + A50966D52C4DD79000A4E5B5 /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966D42C4DD79000A4E5B5 /* TokenTests.swift */; }; + A50966DB2C4DDFAE00A4E5B5 /* PkceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966DA2C4DDFAE00A4E5B5 /* PkceTests.swift */; }; + A50966DD2C4DE4D700A4E5B5 /* MockResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966DC2C4DE4D700A4E5B5 /* MockResponse.swift */; }; + A50966DF2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966DE2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift */; }; + A50966E12C4F1D9D00A4E5B5 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966E02C4F1D9D00A4E5B5 /* MockURLProtocol.swift */; }; + A50966E32C4F2F4300A4E5B5 /* OidcClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966E22C4F2F4300A4E5B5 /* OidcClientTests.swift */; }; + A50966EF2C4FF75400A4E5B5 /* MockStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966EE2C4FF75400A4E5B5 /* MockStorage.swift */; }; + A50967092C50843E00A4E5B5 /* MockAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50967082C50843E00A4E5B5 /* MockAPIEndpoint.swift */; }; + A50981BE2CEBDF1700F4B487 /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A50981BD2CEBDF1700F4B487 /* PingOrchestrate.framework */; }; + A50981BF2CEBDF1700F4B487 /* PingOrchestrate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A50981BD2CEBDF1700F4B487 /* PingOrchestrate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A5A712442CAC520500B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A712432CAC520500B7DD58 /* PrivacyInfo.xcprivacy */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3AB1CA112BD6F99C003FCE3C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AB1C9FE2BD6F99C003FCE3C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3AB1CA062BD6F99C003FCE3C; + remoteInfo = PingOidc; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A50981C02CEBDF1700F4B487 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + A50981BF2CEBDF1700F4B487 /* PingOrchestrate.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3AB1CA072BD6F99C003FCE3C /* PingOidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingOidc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AB1CA0A2BD6F99C003FCE3C /* Oidc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Oidc.h; sourceTree = ""; }; + 3AB1CA0F2BD6F99C003FCE3C /* OidcTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OidcTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AB1CA2A2BD7727B003FCE3C /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + 3AB1CA2B2BD7727B003FCE3C /* Pkce.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pkce.swift; sourceTree = ""; }; + 3AB1CA302BD859BC003FCE3C /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 3AB1CA322BD8823C003FCE3C /* Agent.swift */ = {isa = PBXFileReference; indentWidth = 5; lastKnownFileType = sourcecode.swift; path = Agent.swift; sourceTree = ""; }; + 3AB1CA342BD884E4003FCE3C /* OidcClientConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcClientConfig.swift; sourceTree = ""; }; + 3AB1CA3A2BD96089003FCE3C /* OidcUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcUser.swift; sourceTree = ""; }; + 3AB1CA3C2BD961A2003FCE3C /* OidcClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcClient.swift; sourceTree = ""; }; + A50966B52C4C342800A4E5B5 /* OidcError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcError.swift; sourceTree = ""; }; + A50966B72C4C34CB00A4E5B5 /* AuthCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCode.swift; sourceTree = ""; }; + A50966B92C4C3E7300A4E5B5 /* OpenIdConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenIdConfiguration.swift; sourceTree = ""; }; + A50966D42C4DD79000A4E5B5 /* TokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTests.swift; sourceTree = ""; }; + A50966DA2C4DDFAE00A4E5B5 /* PkceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PkceTests.swift; sourceTree = ""; }; + A50966DC2C4DE4D700A4E5B5 /* MockResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockResponse.swift; sourceTree = ""; }; + A50966DE2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcClientConfigTests.swift; sourceTree = ""; }; + A50966E02C4F1D9D00A4E5B5 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; + A50966E22C4F2F4300A4E5B5 /* OidcClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcClientTests.swift; sourceTree = ""; }; + A50966EE2C4FF75400A4E5B5 /* MockStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStorage.swift; sourceTree = ""; }; + A50967082C50843E00A4E5B5 /* MockAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIEndpoint.swift; sourceTree = ""; }; + A50981BD2CEBDF1700F4B487 /* PingOrchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOrchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A5A712432CAC520500B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3AB1CA042BD6F99C003FCE3C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A50981BE2CEBDF1700F4B487 /* PingOrchestrate.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AB1CA0C2BD6F99C003FCE3C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AB1CA102BD6F99C003FCE3C /* PingOidc.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3A8532272BE2C5DB00F8619D /* Frameworks */ = { + isa = PBXGroup; + children = ( + A50981BD2CEBDF1700F4B487 /* PingOrchestrate.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3AB1C9FD2BD6F99C003FCE3C = { + isa = PBXGroup; + children = ( + 3AB1CA092BD6F99C003FCE3C /* Oidc */, + 3AB1CA132BD6F99C003FCE3C /* OidcTests */, + 3AB1CA082BD6F99C003FCE3C /* Products */, + 3A8532272BE2C5DB00F8619D /* Frameworks */, + ); + sourceTree = ""; + }; + 3AB1CA082BD6F99C003FCE3C /* Products */ = { + isa = PBXGroup; + children = ( + 3AB1CA072BD6F99C003FCE3C /* PingOidc.framework */, + 3AB1CA0F2BD6F99C003FCE3C /* OidcTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 3AB1CA092BD6F99C003FCE3C /* Oidc */ = { + isa = PBXGroup; + children = ( + A5A712432CAC520500B7DD58 /* PrivacyInfo.xcprivacy */, + 3AB1CA0A2BD6F99C003FCE3C /* Oidc.h */, + 3AB1CA322BD8823C003FCE3C /* Agent.swift */, + A50966B72C4C34CB00A4E5B5 /* AuthCode.swift */, + 3AB1CA3C2BD961A2003FCE3C /* OidcClient.swift */, + 3AB1CA342BD884E4003FCE3C /* OidcClientConfig.swift */, + A50966B52C4C342800A4E5B5 /* OidcError.swift */, + 3AB1CA3A2BD96089003FCE3C /* OidcUser.swift */, + A50966B92C4C3E7300A4E5B5 /* OpenIdConfiguration.swift */, + 3AB1CA2B2BD7727B003FCE3C /* Pkce.swift */, + 3AB1CA2A2BD7727B003FCE3C /* Token.swift */, + 3AB1CA302BD859BC003FCE3C /* User.swift */, + ); + path = Oidc; + sourceTree = ""; + }; + 3AB1CA132BD6F99C003FCE3C /* OidcTests */ = { + isa = PBXGroup; + children = ( + A509D1CC2C6D04E9003A0006 /* mock */, + A50966DE2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift */, + A50966E22C4F2F4300A4E5B5 /* OidcClientTests.swift */, + A50966DA2C4DDFAE00A4E5B5 /* PkceTests.swift */, + A50966D42C4DD79000A4E5B5 /* TokenTests.swift */, + ); + path = OidcTests; + sourceTree = ""; + }; + A509D1CC2C6D04E9003A0006 /* mock */ = { + isa = PBXGroup; + children = ( + A50967082C50843E00A4E5B5 /* MockAPIEndpoint.swift */, + A50966DC2C4DE4D700A4E5B5 /* MockResponse.swift */, + A50966E02C4F1D9D00A4E5B5 /* MockURLProtocol.swift */, + A50966EE2C4FF75400A4E5B5 /* MockStorage.swift */, + ); + path = mock; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3AB1CA022BD6F99C003FCE3C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AB1CA162BD6F99C003FCE3C /* Oidc.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3AB1CA062BD6F99C003FCE3C /* PingOidc */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3AB1CA192BD6F99C003FCE3C /* Build configuration list for PBXNativeTarget "PingOidc" */; + buildPhases = ( + 3AB1CA022BD6F99C003FCE3C /* Headers */, + 3AB1CA032BD6F99C003FCE3C /* Sources */, + 3AB1CA042BD6F99C003FCE3C /* Frameworks */, + 3AB1CA052BD6F99C003FCE3C /* Resources */, + A50981C02CEBDF1700F4B487 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PingOidc; + productName = PingOidc; + productReference = 3AB1CA072BD6F99C003FCE3C /* PingOidc.framework */; + productType = "com.apple.product-type.framework"; + }; + 3AB1CA0E2BD6F99C003FCE3C /* OidcTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3AB1CA1C2BD6F99C003FCE3C /* Build configuration list for PBXNativeTarget "OidcTests" */; + buildPhases = ( + 3AB1CA0B2BD6F99C003FCE3C /* Sources */, + 3AB1CA0C2BD6F99C003FCE3C /* Frameworks */, + 3AB1CA0D2BD6F99C003FCE3C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3AB1CA122BD6F99C003FCE3C /* PBXTargetDependency */, + ); + name = OidcTests; + productName = PingOidcTests; + productReference = 3AB1CA0F2BD6F99C003FCE3C /* OidcTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3AB1C9FE2BD6F99C003FCE3C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + 3AB1CA062BD6F99C003FCE3C = { + CreatedOnToolsVersion = 15.0; + LastSwiftMigration = 1500; + }; + 3AB1CA0E2BD6F99C003FCE3C = { + CreatedOnToolsVersion = 15.0; + LastSwiftMigration = 1540; + }; + }; + }; + buildConfigurationList = 3AB1CA012BD6F99C003FCE3C /* Build configuration list for PBXProject "Oidc" */; + compatibilityVersion = "Xcode 15.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3AB1C9FD2BD6F99C003FCE3C; + productRefGroup = 3AB1CA082BD6F99C003FCE3C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3AB1CA062BD6F99C003FCE3C /* PingOidc */, + 3AB1CA0E2BD6F99C003FCE3C /* OidcTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3AB1CA052BD6F99C003FCE3C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A712442CAC520500B7DD58 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AB1CA0D2BD6F99C003FCE3C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3AB1CA032BD6F99C003FCE3C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AB1CA312BD859BC003FCE3C /* User.swift in Sources */, + 3AB1CA3D2BD961A2003FCE3C /* OidcClient.swift in Sources */, + 3AB1CA332BD8823C003FCE3C /* Agent.swift in Sources */, + A50966B82C4C34CB00A4E5B5 /* AuthCode.swift in Sources */, + 3AB1CA3B2BD96089003FCE3C /* OidcUser.swift in Sources */, + A50966BA2C4C3E7300A4E5B5 /* OpenIdConfiguration.swift in Sources */, + 3AB1CA2E2BD7727B003FCE3C /* Pkce.swift in Sources */, + A50966B62C4C342800A4E5B5 /* OidcError.swift in Sources */, + 3AB1CA2D2BD7727B003FCE3C /* Token.swift in Sources */, + 3AB1CA352BD884E4003FCE3C /* OidcClientConfig.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AB1CA0B2BD6F99C003FCE3C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A50967092C50843E00A4E5B5 /* MockAPIEndpoint.swift in Sources */, + A50966DB2C4DDFAE00A4E5B5 /* PkceTests.swift in Sources */, + A50966EF2C4FF75400A4E5B5 /* MockStorage.swift in Sources */, + A50966DF2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift in Sources */, + A50966E12C4F1D9D00A4E5B5 /* MockURLProtocol.swift in Sources */, + A50966DD2C4DE4D700A4E5B5 /* MockResponse.swift in Sources */, + A50966D52C4DD79000A4E5B5 /* TokenTests.swift in Sources */, + A50966E32C4F2F4300A4E5B5 /* OidcClientTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3AB1CA122BD6F99C003FCE3C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3AB1CA062BD6F99C003FCE3C /* PingOidc */; + targetProxy = 3AB1CA112BD6F99C003FCE3C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 3AB1CA172BD6F99C003FCE3C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 3AB1CA182BD6F99C003FCE3C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 3AB1CA1A2BD6F99C003FCE3C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Oidc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3AB1CA1B2BD6F99C003FCE3C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Oidc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3AB1CA1D2BD6F99C003FCE3C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.OidcTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Debug; + }; + 3AB1CA1E2BD6F99C003FCE3C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.OidcTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3AB1CA012BD6F99C003FCE3C /* Build configuration list for PBXProject "Oidc" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AB1CA172BD6F99C003FCE3C /* Debug */, + 3AB1CA182BD6F99C003FCE3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3AB1CA192BD6F99C003FCE3C /* Build configuration list for PBXNativeTarget "PingOidc" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AB1CA1A2BD6F99C003FCE3C /* Debug */, + 3AB1CA1B2BD6F99C003FCE3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3AB1CA1C2BD6F99C003FCE3C /* Build configuration list for PBXNativeTarget "OidcTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AB1CA1D2BD6F99C003FCE3C /* Debug */, + 3AB1CA1E2BD6F99C003FCE3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3AB1C9FE2BD6F99C003FCE3C /* Project object */; +} diff --git a/Oidc/Oidc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Oidc/Oidc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Oidc/Oidc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Oidc/Oidc.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Oidc/Oidc.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..16cf018 --- /dev/null +++ b/Oidc/Oidc.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,17 @@ + + + + + FILEHEADER + +// ___FILENAME___ +// ___PACKAGENAME___ +// +// Copyright (c) ___YEAR___ Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + + diff --git a/Oidc/Oidc.xcodeproj/xcshareddata/xcschemes/Oidc.xcscheme b/Oidc/Oidc.xcodeproj/xcshareddata/xcschemes/Oidc.xcscheme new file mode 100644 index 0000000..5817830 --- /dev/null +++ b/Oidc/Oidc.xcodeproj/xcshareddata/xcschemes/Oidc.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Oidc/Oidc/Agent.swift b/Oidc/Oidc/Agent.swift new file mode 100644 index 0000000..9205bb3 --- /dev/null +++ b/Oidc/Oidc/Agent.swift @@ -0,0 +1,120 @@ +// +// Agent.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// The `Agent` is a protocol that is used to authenticate and end a session with an OpenID Connect provider. +/// `T` is the configuration object that is used to configure the `Agent`. +public protocol Agent { + associatedtype T + + /// Provides the configuration object for the `Agent`. + /// - Returns: A function that returns the configuration object. + func config() -> () -> T + + /// End the session with the OpenID Connect provider. + /// Best effort is made to end the session. + func endSession(oidcConfig: OidcConfig, idToken: String) async throws -> Bool + + /// Authorize the `Agent` with the OpenID Connect provider. + /// Before returning the `AuthCode`, the agent should verify the response from the OpenID Connect provider. + /// For example, the agent should verify the state parameter in the response. + /// - Parameter oidcConfig: The configuration for the OpenID Connect client. + /// - Returns: `AuthCode` instance + func authorize(oidcConfig: OidcConfig) async throws -> AuthCode +} + + +/// Allow the `Agent` to run on `OidcConfig` so that it can access the configuration object. +public class OidcConfig { + let oidcClientConfig: OidcClientConfig + let config: T + + /// Initialize the `OidcConfig` with the `OidcClientConfig` and the configuration object. + /// - Parameters: + /// - oidcClientConfig: The client configuration for the OpenID Connect provider. + /// - config: The configuration object. + init(oidcClientConfig: OidcClientConfig, config: T) { + self.oidcClientConfig = oidcClientConfig + self.config = config + } +} + + +/// Default implementation of the `Agent` interface. +public class DefaultAgent: Agent { + + public typealias T = Void + + /// Initialize the `DefaultAgent`. + public init() {} + + /// Provides an empty configuration for the `DefaultAgent`. + /// - Returns: A function that returns Void + public func config() -> () -> Void { + return {} + } + + /// End the session with the OpenID Connect provider. This implementation always returns false. + /// - Parameters: + /// - oidcConfig: The configuration for the OpenID Connect client. + /// - idToken: The ID token used to end the session. + /// - Returns: Always returns false. + public func endSession(oidcConfig: OidcConfig, idToken: String) async -> Bool { + return false + } + + /// Authorize the `DefaultAgent` with the OpenID Connect provider. + /// This implementation always throws an `OidcError.authorizeError` error. + /// - Parameter oidcConfig: The configuration for the OpenID Connect client. + /// - Returns: Never returns normally. + public func authorize(oidcConfig: OidcConfig) async throws -> AuthCode { + throw OidcError.authorizeError(message: "No AuthCode is available.") + } +} + + +/// Delegate protocol to dispatch `Agent` functions +public protocol AgentDelegateProtocol { + associatedtype T + func authenticate() async throws -> AuthCode + func endSession(idToken: String) async throws -> Bool +} + + +/// Delegate class to dispatch `Agent` functions +public class AgentDelegate: AgentDelegateProtocol { + let agent: any Agent + let oidcConfig: OidcConfig + + /// Initialize the `AgentDelegate` with an `Agent` and the configuration object. + /// - Parameters: + /// - agent: The `Agent` instance. + /// - agentConfig: The configuration object for the `Agent`. + /// - oidcClientConfig: The `OidcClientConfig` instance . + init(agent: any Agent, agentConfig: T, oidcClientConfig: OidcClientConfig) { + self.agent = agent + self.oidcConfig = OidcConfig(oidcClientConfig: oidcClientConfig, config: agentConfig) + } + + /// Authenticate with the OpenID Connect provider. + /// - Returns: The authorization code. + public func authenticate() async throws -> AuthCode { + return try await self.agent.authorize(oidcConfig: oidcConfig) + } + + /// End the session with the OpenID Connect provider. + /// - Parameter idToken: The ID token used to end the session. + /// - Returns: A boolean indicating whether the session was successfully ended. + public func endSession(idToken: String) async throws -> Bool { + return try await agent.endSession(oidcConfig: oidcConfig, idToken: idToken) + } +} diff --git a/Oidc/Oidc/AuthCode.swift b/Oidc/Oidc/AuthCode.swift new file mode 100644 index 0000000..192dc54 --- /dev/null +++ b/Oidc/Oidc/AuthCode.swift @@ -0,0 +1,27 @@ +// +// AuthCode.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Struct representing an authorization code. +public struct AuthCode: Codable { + let code: String + let codeVerifier: String? + + /// Initializes a new instance of `AuthCode`. + /// - Parameters: + /// - code: The authorization code as a string. Default is an empty string. + /// - codeVerifier: An optional code verifier associated with the authorization code. Default is `nil`. + public init(code: String = "", codeVerifier: String? = nil) { + self.code = code + self.codeVerifier = codeVerifier + } +} diff --git a/Oidc/Oidc/Oidc.h b/Oidc/Oidc/Oidc.h new file mode 100644 index 0000000..8f9781a --- /dev/null +++ b/Oidc/Oidc/Oidc.h @@ -0,0 +1,21 @@ +// +// Oidc.h +// Oidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +#import + +//! Project version number for Oidc. +FOUNDATION_EXPORT double OidcVersionNumber; + +//! Project version string for Oidc. +FOUNDATION_EXPORT const unsigned char OidcVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Oidc/Oidc/OidcClient.swift b/Oidc/Oidc/OidcClient.swift new file mode 100644 index 0000000..8a2bd4c --- /dev/null +++ b/Oidc/Oidc/OidcClient.swift @@ -0,0 +1,267 @@ +// +// OidcClient.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingLogger +import PingOrchestrate + +/// Class representing an OpenID Connect client. +public class OidcClient { + private let config: OidcClientConfig + private let logger: Logger + + /// OidcClient initializer. + /// - Parameter config: The configuration for this client. + public init(config: OidcClientConfig) { + self.config = config + self.logger = config.logger + } + + /// Retrieves an access token. If a cached token is available and not expired, it is returned. + /// Otherwise, a new token is fetched with refresh token if refresh grant is available. + /// - Returns: A Result containing the access token or an error. + public func token() async -> Result { + + do { + try await config.oidcInitialize() + } catch { + return .failure((error as? OidcError) ?? OidcError.unknown(cause: error)) + } + + config.logger.i("Getting access token") + if let cached = try? await config.storage.get() { + if !cached.isExpired(threshold: config.refreshThreshold) { + config.logger.i("Token is not expired. Returning cached token.") + return .success(cached) + } + config.logger.i("Token is expired. Attempting to refresh.") + if let cachedefreshToken = cached.refreshToken { + do { + let refreshedToken = try await refreshToken(cachedefreshToken) + return .success(refreshedToken) + } catch { + config.logger.e("Failed to refresh token. Revoking token and re-authenticating.", error: error) + await revoke(cached) + } + } + } + + // Authenticate the user + do { + let code = try await config.agent?.authenticate() + if let unWrappedcode = code { + let token = try await exchangeToken(unWrappedcode) + try await config.storage.save(item: token) + return .success(token) + } else { + return .failure(OidcError.authorizeError(message: "Authorization code not found")) + } + + } catch { + return .failure((error as? OidcError) ?? (OidcError.authorizeError(cause: error))) + } + } + + /// Refreshes the access token. + /// - Parameter refreshToken: The refresh token to use for refreshing the access token. + /// - Returns: The refreshed access token. + private func refreshToken(_ refreshToken: String) async throws -> Token { + try await config.oidcInitialize() + config.logger.i("Refreshing token") + + let params = [ + Constants.grant_type: Constants.refresh_token, + Constants.refresh_token: refreshToken, + Constants.client_id: config.clientId + ] + + guard let httpClient = config.httpClient else { + throw OidcError.networkError(message: "HTTP client not found") + } + + guard let openId = config.openId else { + throw OidcError.unknown(message: "OpenID configuration not found") + } + + let request = Request() + request.url(openId.tokenEndpoint) + request.form(formData: params) + + let (data, response) = try await httpClient.sendRequest(request: request) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw OidcError.apiError(code: (response as? HTTPURLResponse)?.statusCode ?? 0, message: String(decoding: data, as: UTF8.self)) + } + let token = try JSONDecoder().decode(Token.self, from: data) + + try await config.storage.save(item: token) + + return token + } + + /// Revokes the access token. + public func revoke() async { + await revoke(nil) + } + + /// Revokes a specific access token. Best effort to revoke the token. + /// The stored token is removed regardless of the result. + /// - Parameter token: The access token to revoke. If null, the currently stored token is revoked. + private func revoke(_ token: Token? = nil) async { + var accessToken = token + if accessToken == nil { + accessToken = try? await config.storage.get() + } + if let token = accessToken { + do { + try await config.storage.delete() + try await config.oidcInitialize() + } catch { + config.logger.e("Failed to delete token", error: error) + } + let t = token.refreshToken ?? token.accessToken + let params = [ + Constants.client_id: config.clientId, + Constants.token: t + ] + + guard let httpClient = config.httpClient else { + config.logger.e("HTTP client not found", error: nil) + return + } + + guard let openId = config.openId else { + config.logger.e("OpenID configuration not found", error: nil) + return + } + + let request = Request() + request.url(openId.revocationEndpoint) + request.form(formData: params) + do { + let (_, _) = try await httpClient.sendRequest(request: request) + } catch { + config.logger.e("Failed to revoke token", error: error) + } + } + } + + /// Ends the session. Best effort to end the session. + /// The stored token is removed regardless of the result. + /// - Returns: A boolean indicating whether the session was ended successfully. + public func endSession() async -> Bool { + return await endSession { idToken in + return try await self.config.agent?.endSession(idToken: idToken) ?? false + } + } + + /// Ends the session with a custom sign-off procedure. + /// - Parameter signOff: A suspend function to perform the sign-off. + /// - Returns: A boolean indicating whether the session was ended successfully. + public func endSession(signOff: @escaping (String) async throws -> Bool) async -> Bool { + do { + try await config.oidcInitialize() + if let accessToken = try await config.storage.get() { + await revoke(accessToken) + if let idToken = accessToken.idToken { + return try await signOff(idToken) + } + } + } catch { + config.logger.e("Failed to end session", error: error) + return false + } + return true + } + + /// Retrieves user information. + /// - Returns: A Result containing the user information or an error. + public func userinfo() async -> Result { + do { + try await config.oidcInitialize() + + guard let httpClient = config.httpClient else { + throw OidcError.networkError(message: "HTTP client not found") } + + guard let openId = config.openId else { + throw OidcError.unknown(message: "OpenID configuration not found") + } + + switch await token() { + case .failure(let error): + return .failure(error) + case .success(let token): + let request = Request() + request.url(openId.userinfoEndpoint) + request.header(name: "Authorization", value: "Bearer \(token.accessToken)") + let (data, response) = try await httpClient.sendRequest(request: request) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw OidcError.apiError(code: (response as? HTTPURLResponse)?.statusCode ?? 0, message: String(decoding: data, as: UTF8.self)) + } + let json = try JSONSerialization.jsonObject(with: data, options: []) as? UserInfo ?? [:] + return .success(json) + } + } catch { + return .failure((error as? OidcError) ?? .unknown(cause: error)) + } + } + + /// Exchanges an authorization code for an access token. + /// - Parameter authCode: The authorization code to exchange. + /// - Returns: The access token. + private func exchangeToken(_ authCode: AuthCode) async throws -> Token { + try await config.oidcInitialize() + config.logger.i("Exchanging token") + + guard let httpClient = config.httpClient else { + throw OidcError.networkError(message: "HTTP client not found") + } + + guard let openId = config.openId else { + throw OidcError.unknown(message: "OpenID configuration not found") + } + + var params = [ + Constants.grant_type: Constants.authorization_code, + Constants.code: authCode.code, + Constants.redirect_uri: config.redirectUri, + Constants.client_id: config.clientId, + ] + + if let codeVerifier = authCode.codeVerifier { + params[Constants.code_verifier] = codeVerifier + } + + let request = Request() + request.url(openId.tokenEndpoint) + request.form(formData: params) + let (data, response) = try await httpClient.sendRequest(request: request) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw OidcError.apiError(code: (response as? HTTPURLResponse)?.statusCode ?? 0, message: String(decoding: data, as: UTF8.self)) + } + let token = try JSONDecoder().decode(Token.self, from: data) + return token + } + + public enum Constants { + public static let client_id = "client_id" + public static let grant_type = "grant_type" + public static let refresh_token = "refresh_token" + public static let token = "token" + public static let authorization_code = "authorization_code" + public static let redirect_uri = "redirect_uri" + public static let code_verifier = "code_verifier" + public static let code = "code" + public static let id_token_hint = "id_token_hint" + } +} diff --git a/Oidc/Oidc/OidcClientConfig.swift b/Oidc/Oidc/OidcClientConfig.swift new file mode 100644 index 0000000..5f8ffd6 --- /dev/null +++ b/Oidc/Oidc/OidcClientConfig.swift @@ -0,0 +1,143 @@ +// +// OidcClientConfig.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingOrchestrate +import PingLogger +import PingStorage + +/// Configuration class for OIDC client. +public class OidcClientConfig { + /// OpenID configuration. + public var openId: OpenIdConfiguration? + /// Token refresh threshold in seconds. + public var refreshThreshold: Int64 = 0 + /// Agent delegate for handling OIDC operations. + internal var agent: (any AgentDelegateProtocol)? + /// Logger instance for logging. + public var logger: Logger + /// Storage delegate for storing tokens. + public var storage: StorageDelegate + /// Discovery endpoint URL. + public var discoveryEndpoint = "" + /// Client ID for OIDC. + public var clientId = "" + /// Set of scopes for OIDC. + public var scopes = Set() + /// Redirect URI for OIDC. + public var redirectUri = "" + /// Login hint for OIDC. + public var loginHint: String? + /// State parameter for OIDC. + public var state: String? + /// Nonce parameter for OIDC. + public var nonce: String? + /// Display parameter for OIDC. + public var display: String? + /// Prompt parameter for OIDC. + public var prompt: String? + /// UI locales parameter for OIDC. + public var uiLocales: String? + /// ACR values parameter for OIDC. + public var acrValues: String? + /// Additional parameters for OIDC. + public var additionalParameters = [String: String]() + /// HTTP client for making network requests. + public var httpClient: HttpClient? + + /// Initializes a new `OidcClientConfig` instance. + public init() { + logger = LogManager.none + storage = KeychainStorage(account: "ACCESS_TOKEN_STORAGE", encryptor: SecuredKeyEncryptor() ?? NoEncryptor(), cacheable: true) + } + + /// Adds a scope to the set of scopes. + /// - Parameter scope: The scope to add. + public func scope(_ scope: String) { + scopes.insert(scope) + } + + /// Updates the agent with the provided configuration. + /// - Parameters: + /// - agent: The agent to update. + /// - config: The configuration block for the agent. + public func updateAgent(_ agent: any Agent, config: (T) -> Void = {_ in }) { + self.agent = AgentDelegate(agent: agent, agentConfig: agent.config()(), oidcClientConfig: self) + + } + + /// Initializes the lazy properties to their default values. + public func oidcInitialize() async throws { + if httpClient == nil { + httpClient = HttpClient() + } + + if openId != nil { + return + } + + openId = try await discover() + } + + /// Discovers the OpenID configuration from the discovery endpoint. + /// - Returns: The discovered OpenID configuration. + private func discover() async throws -> OpenIdConfiguration? { + guard URL(string: discoveryEndpoint) != nil else { + logger.e("Invalid Discovery URL", error: nil) + return nil + } + + guard let httpClient else { + logger.e("Invalid Http Client URL", error: nil) + return nil + } + let request = Request() + request.url(discoveryEndpoint) + let (data, response) = try await httpClient.sendRequest(request: request) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw OidcError.apiError(code: (response as? HTTPURLResponse)?.statusCode ?? 500, message: String(decoding: data, as: UTF8.self)) + } + let configuration = try JSONDecoder().decode(OpenIdConfiguration.self, from: data) + return configuration + } + + /// Clones the current configuration. + /// - Returns: A new instance of OidcClientConfig with the same properties. + public func clone() -> OidcClientConfig { + let cloned = OidcClientConfig() + cloned.update(with: self) + return cloned + } + + /// Merges another configuration into this one. + /// - Parameter other: The other configuration to merge. + func update(with other: OidcClientConfig) { + self.openId = other.openId + self.refreshThreshold = other.refreshThreshold + self.agent = other.agent + self.logger = other.logger + self.storage = other.storage + self.discoveryEndpoint = other.discoveryEndpoint + self.clientId = other.clientId + self.scopes = other.scopes + self.redirectUri = other.redirectUri + self.loginHint = other.loginHint + self.state = other.state + self.nonce = other.nonce + self.display = other.display + self.prompt = other.prompt + self.uiLocales = other.uiLocales + self.acrValues = other.acrValues + self.additionalParameters = other.additionalParameters + self.httpClient = other.httpClient + } +} diff --git a/Oidc/Oidc/OidcError.swift b/Oidc/Oidc/OidcError.swift new file mode 100644 index 0000000..2c0ad60 --- /dev/null +++ b/Oidc/Oidc/OidcError.swift @@ -0,0 +1,54 @@ +// +// OidcError.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Enum for OIDC errors. +public enum OidcError: LocalizedError { + /// An error that occurs during the authorization process. + /// - Parameters: + /// - cause: The underlying error that caused the issue (optional). + /// - message: A descriptive message about the error (optional). + case authorizeError(cause: Error? = nil, message: String? = nil) + + /// An error that occurs during network communication. + /// - Parameters: + /// - cause: The underlying error that caused the issue (optional). + /// - message: A descriptive message about the error (optional). + case networkError(cause: Error? = nil, message: String? = nil) + + /// An error returned from the API. + /// - Parameters: + /// - code: The HTTP status code of the error. + /// - message: A descriptive message about the error. + case apiError(code: Int, message: String) + + /// An unknown or unspecified error. + /// - Parameters: + /// - cause: The underlying error that caused the issue (optional). + /// - message: A descriptive message about the error (optional). + case unknown(cause: Error? = nil, message: String? = nil) + + /// Provides a human-readable description of the error. + /// - Returns: A `String` representing the error message. + public var errorMessage: String { + switch self { + case .authorizeError(cause: let cause, message: let message): + return "Authorization error: \(message ?? cause?.localizedDescription ?? "Unknown")" + case .networkError(cause: let cause, message: let message): + return "Network error: \(message ?? cause?.localizedDescription ?? "Unknown")" + case .apiError(code: let code, message: let message): + return "API error: \(code) \(message)" + case .unknown(cause: let cause, message: let message): + return "Error: \(message ?? cause?.localizedDescription ?? "Unknown")" + } + } +} diff --git a/Oidc/Oidc/OidcUser.swift b/Oidc/Oidc/OidcUser.swift new file mode 100644 index 0000000..8654853 --- /dev/null +++ b/Oidc/Oidc/OidcUser.swift @@ -0,0 +1,52 @@ +// +// OidcUser.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +/// Class for an OIDC User +public class OidcUser: User { + private var userinfo: UserInfo? + private let oidcClient: OidcClient + + /// OidcUser initializer + /// - Parameter config: The configuration for the OIDC client. + public init(config: OidcClientConfig) { + self.oidcClient = OidcClient(config: config) + } + + /// Gets the token for the user. + /// - Returns: The token for the user. + public func token() async -> Result { + return await oidcClient.token() + } + + /// Revokes the user's token. + public func revoke() async { + await oidcClient.revoke() + } + + /// Gets the user information. + /// - Parameter cache: Whether to cache the user information. + /// - Returns: The user information. + public func userinfo(cache: Bool = true) async -> Result { + if let userinfo = self.userinfo, cache { + return .success(userinfo) + } + let result = await oidcClient.userinfo() + if case .success(let data) = result, cache { + self.userinfo = data + } + return result + } + + /// Logs out the user. + public func logout() async { + _ = await oidcClient.endSession() + } +} diff --git a/Oidc/Oidc/OpenIdConfiguration.swift b/Oidc/Oidc/OpenIdConfiguration.swift new file mode 100644 index 0000000..ee6688b --- /dev/null +++ b/Oidc/Oidc/OpenIdConfiguration.swift @@ -0,0 +1,35 @@ +// +// OpenIdConfiguration.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Struct representing the OpenID Connect configuration. +public struct OpenIdConfiguration: Codable { + /// The URL of the authorization endpoint. + public let authorizationEndpoint: String + /// The URL of the token endpoint. + public let tokenEndpoint: String + /// The URL of the userinfo endpoint. + public let userinfoEndpoint: String + /// The URL of the end session endpoint. + public let endSessionEndpoint: String + /// The URL of the revocation endpoint . + public let revocationEndpoint: String + + // Define CodingKeys enum to map serialized names to property names + private enum CodingKeys: String, CodingKey { + case authorizationEndpoint = "authorization_endpoint" + case tokenEndpoint = "token_endpoint" + case userinfoEndpoint = "userinfo_endpoint" + case endSessionEndpoint = "end_session_endpoint" + case revocationEndpoint = "revocation_endpoint" + } +} diff --git a/Oidc/Oidc/Pkce.swift b/Oidc/Oidc/Pkce.swift new file mode 100644 index 0000000..27c2784 --- /dev/null +++ b/Oidc/Oidc/Pkce.swift @@ -0,0 +1,61 @@ +// +// PKCE.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import CryptoKit + +/// Struct for PKCE (Proof Key for Code Exchange). +/// - property codeVerifier: The code verifier for the PKCE. +/// - property codeChallenge: The code challenge for the PKCE. +/// - property codeChallengeMethod: The code challenge method for the PKCE. +public struct Pkce { + public let codeVerifier: String + public let codeChallenge: String + public let codeChallengeMethod: String + + /// Generates a new PKCE. + /// - Returns: A new PKCE. + public static func generate() -> Pkce { + let codeVerifier = generateCodeVerifier() + let codeChallenge = generateCodeChallenge(codeVerifier: codeVerifier) + return Pkce(codeVerifier: codeVerifier, codeChallenge: codeChallenge, codeChallengeMethod: "S256") + } + + /// Generates a new code verifier for the PKCE. + /// - returns: A new code verifier. + private static func generateCodeVerifier() -> String { + var bytes = [UInt8](repeating: 0, count: 64) + _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + return Data(bytes).base64URLEncodedString() // remove padding as per https://tools.ietf.org/html/rfc7636#section-4.1 + } + + /// Generates a new code challenge for the PKCE. + /// - Parameter codeVerifier: The code verifier for the PKCE. + /// - Returns: A new code challenge. + private static func generateCodeChallenge(codeVerifier: String) -> String { + guard let data = codeVerifier.data(using: .utf8) else { + fatalError("Unable to convert code verifier to data") + } + let digest = SHA256.hash(data: data) + return Data(digest).base64URLEncodedString() // remove padding as per https://tools.ietf.org/html/rfc7636#section-4.1 + } +} + + +extension Data { + func base64URLEncodedString() -> String { + return base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + .trimmingCharacters(in: .whitespaces) + } +} diff --git a/Oidc/Oidc/PrivacyInfo.xcprivacy b/Oidc/Oidc/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..fcfc9b9 --- /dev/null +++ b/Oidc/Oidc/PrivacyInfo.xcprivacy @@ -0,0 +1,10 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/Oidc/Oidc/Token.swift b/Oidc/Oidc/Token.swift new file mode 100644 index 0000000..cc4e489 --- /dev/null +++ b/Oidc/Oidc/Token.swift @@ -0,0 +1,112 @@ +// +// Token.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Struct representing an OIDC token. +public struct Token: Codable { + /// The access token used for authentication. + public let accessToken: String + /// The type of token. + public let tokenType: String? + /// The scope of access granted by the token. + public let scope: String? + /// The duration of the token's validity in seconds + public let expiresIn: Int64 + /// The refresh token used to obtain a new access token. + public let refreshToken: String? + /// The ID token + public let idToken: String? + /// The exact timestamp (in seconds since 1970) when the token expires. + public let expiresAt: Int64 + + /// Initializes a new instance of `Token`. + /// - Parameters: + /// - accessToken: The access token string. + /// - tokenType: The type of token. + /// - scope: The scope of access granted by the token. + /// - expiresIn: The duration (in seconds) for which the token is valid. + /// - refreshToken: The refresh token string (optional). + /// - idToken: The ID token string (optional). + public init(accessToken: String, tokenType: String?, scope: String?, expiresIn: Int64, refreshToken: String?, idToken: String?) { + self.accessToken = accessToken + self.tokenType = tokenType + self.scope = scope + self.expiresIn = expiresIn + self.refreshToken = refreshToken + self.idToken = idToken + self.expiresAt = Int64(Date().timeIntervalSince1970) + expiresIn + } + + /// A Boolean value indicating whether the token has expired. + /// - Returns: `true` if the current time is greater than or equal to the token's expiry time; otherwise, `false`. + public var isExpired: Bool { + return Int64(Date().timeIntervalSince1970) >= expiresAt + } + + /// Checks if the token will expire within a specified threshold. + /// - Parameter threshold: The threshold duration (in seconds) to check for expiration. + /// - Returns: `true` if the token will expire within the threshold; otherwise, `false`. + public func isExpired(threshold: Int64) -> Bool { + return Int64(Date().timeIntervalSince1970) >= expiresAt - threshold + } + + /// Decodes a `Token` instance from a decoder. + /// - Parameter decoder: The decoder instance used for decoding. + /// - Throws: An error if decoding fails. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + accessToken = try container.decode(String.self, forKey: .accessToken) + tokenType = try container.decodeIfPresent(String.self, forKey: .tokenType) + scope = try container.decodeIfPresent(String.self, forKey: .scope) + expiresIn = try container.decode(Int64.self, forKey: .expiresIn) + refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken) + idToken = try container.decodeIfPresent(String.self, forKey: .idToken) + expiresAt = try container.decodeIfPresent(Int64.self, forKey: .expiresAt) ?? Int64(Date().timeIntervalSince1970) + expiresIn + } + + /// Encodes the `Token` instance to an encoder. + /// - Parameter encoder: The encoder instance used for encoding. + /// - Throws: An error if encoding fails. + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(accessToken, forKey: .accessToken) + try container.encode(tokenType, forKey: .tokenType) + try container.encode(scope, forKey: .scope) + try container.encode(expiresIn, forKey: .expiresIn) + try container.encode(refreshToken, forKey: .refreshToken) + try container.encode(idToken, forKey: .idToken) + try container.encode(expiresAt, forKey: .expiresAt) + } +} + + +/// Define CodingKeys for the AccessToken struct +extension Token { + /// Coding keys used for encoding and decoding the `Token` struct. + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case tokenType = "token_type" + case scope + case expiresIn = "expires_in" + case refreshToken = "refresh_token" + case idToken = "id_token" + case expiresAt = "expires_at" + } +} + + +extension Token: CustomStringConvertible { + /// A textual representation of the `Token` instance. + public var description: String { + "isExpired: \(isExpired)\n access_token: \(self.accessToken)\n refresh_token: \(refreshToken ?? "nil")\n id_token: \(idToken ?? "nil")\n token_type: \(tokenType ?? "nil")\n scope: \(scope ?? "nil")\n expires_in: \(String(describing: expiresIn))\n expires_at: \(String(describing: Date(timeIntervalSince1970: TimeInterval(expiresAt))))" + } +} diff --git a/Oidc/Oidc/User.swift b/Oidc/Oidc/User.swift new file mode 100644 index 0000000..4d3fcdf --- /dev/null +++ b/Oidc/Oidc/User.swift @@ -0,0 +1,33 @@ +// +// User.swift +// PingOidc +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +/// Protocol for a User. +/// Provides methods for token management, user information retrieval, and logout. +public protocol User { + /// Retrieves the token for the user. + /// - Returns: A `Result` object containing either the `Token` or an `OidcError`. + func token() async -> Result + + /// Revokes the user's token. + func revoke() async + + /// Retrieves the user's information. + /// - Parameter cache: Whether to cache the user information. + /// - Returns: A `Result` object containing either the user information as a `UserInfo` or an `OidcError`. + func userinfo(cache: Bool) async -> Result + + /// Logs out the user. + func logout() async +} + + +/// A type alias representing user information as a dictionary. +public typealias UserInfo = [String: Any] diff --git a/Oidc/OidcTests/OidcClientConfigTests.swift b/Oidc/OidcTests/OidcClientConfigTests.swift new file mode 100644 index 0000000..3fc054a --- /dev/null +++ b/Oidc/OidcTests/OidcClientConfigTests.swift @@ -0,0 +1,208 @@ +// +// OidcClientConfigTests.swift +// OidcTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOidc +@testable import PingOrchestrate +@testable import PingLogger +@testable import PingStorage + +final class OidcClientConfigTests: XCTestCase { + + var oidcClientConfig: OidcClientConfig! + + override func setUp() { + super.setUp() + oidcClientConfig = OidcClientConfig() + oidcClientConfig.discoveryEndpoint = MockAPIEndpoint.discovery.url.absoluteString + oidcClientConfig.storage = MockStorage() + oidcClientConfig.httpClient = HttpClient(session: .shared) + MockURLProtocol.startInterceptingRequests() + } + + override func tearDown() { + oidcClientConfig = nil + MockURLProtocol.stopInterceptingRequests() + super.tearDown() + } + + // TestRailCase(22106) + func testDefaultInitialization() { + oidcClientConfig = OidcClientConfig() + + XCTAssertNil(oidcClientConfig.openId) + XCTAssertEqual(oidcClientConfig.refreshThreshold, 0) + XCTAssertNil(oidcClientConfig.agent) + XCTAssertEqual(oidcClientConfig.discoveryEndpoint, "") + XCTAssertEqual(oidcClientConfig.clientId, "") + XCTAssertTrue(oidcClientConfig.scopes.isEmpty) + XCTAssertEqual(oidcClientConfig.redirectUri, "") + XCTAssertNil(oidcClientConfig.loginHint) + XCTAssertNil(oidcClientConfig.state) + XCTAssertNil(oidcClientConfig.nonce) + XCTAssertNil(oidcClientConfig.display) + XCTAssertNil(oidcClientConfig.prompt) + XCTAssertNil(oidcClientConfig.uiLocales) + XCTAssertNil(oidcClientConfig.acrValues) + XCTAssertTrue(oidcClientConfig.additionalParameters.isEmpty) + XCTAssertNil(oidcClientConfig.httpClient) + } + + func testUpdateAgent() { + let agent = MockAgent() + oidcClientConfig.updateAgent(agent) + XCTAssertNotNil(oidcClientConfig.agent) + } + + // TestRailCase(22118) + func testScopeInsertion() { + oidcClientConfig.scope("openid") + XCTAssertTrue(oidcClientConfig.scopes.contains("openid")) + } + + // TestRailCase(22118) + func testOidcInitializeInvalidDiscovery() async throws { + + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.error) + } + + do { + try await oidcClientConfig.oidcInitialize() + } catch { + XCTAssertNotNil(error) + } + XCTAssertNil(oidcClientConfig.openId) + } + + // TestRailCase(24720) + func testOidcInitializeValidDiscovery() async throws { + + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration) + } + + do { + try await oidcClientConfig.oidcInitialize() + XCTAssertNotNil(oidcClientConfig.openId) + XCTAssertEqual(MockAPIEndpoint.authorization.url.absoluteString, oidcClientConfig.openId!.authorizationEndpoint) + XCTAssertEqual(MockAPIEndpoint.token.url.absoluteString, oidcClientConfig.openId!.tokenEndpoint) + XCTAssertEqual(MockAPIEndpoint.userinfo.url.absoluteString, oidcClientConfig.openId!.userinfoEndpoint) + XCTAssertEqual(MockAPIEndpoint.endSession.url.absoluteString, oidcClientConfig.openId!.endSessionEndpoint) + XCTAssertEqual(MockAPIEndpoint.revocation.url.absoluteString, oidcClientConfig.openId!.revocationEndpoint) + } catch { + XCTFail("Initialization failed with error: \(error)") + } + } + + // TestRailCase(22081) + func testClone() { + oidcClientConfig.refreshThreshold = 100 + oidcClientConfig.agent = AgentDelegate(agent: MockAgent(), agentConfig: (), oidcClientConfig: oidcClientConfig) + oidcClientConfig.logger = LogManager.standard + oidcClientConfig.storage = MockStorage() + oidcClientConfig.discoveryEndpoint = "https://example.com" + oidcClientConfig.clientId = "clientId" + oidcClientConfig.scopes.insert("openid") + oidcClientConfig.redirectUri = "http://localhost/callback" + oidcClientConfig.loginHint = "loginHint" + oidcClientConfig.nonce = "nonce" + oidcClientConfig.display = "display" + oidcClientConfig.prompt = "prompt" + oidcClientConfig.uiLocales = "uiLocales" + oidcClientConfig.acrValues = "acrValues" + oidcClientConfig.additionalParameters = ["param": "value"] + oidcClientConfig.httpClient = HttpClient() + + let clonedConfig = oidcClientConfig.clone() + + XCTAssertEqual(oidcClientConfig.openId.debugDescription, clonedConfig.openId.debugDescription) + XCTAssertEqual(oidcClientConfig.refreshThreshold, clonedConfig.refreshThreshold) + XCTAssertEqual(oidcClientConfig.agent.debugDescription, clonedConfig.agent.debugDescription) + XCTAssertEqual(oidcClientConfig.discoveryEndpoint, clonedConfig.discoveryEndpoint) + XCTAssertEqual(oidcClientConfig.clientId, clonedConfig.clientId) + XCTAssertEqual(oidcClientConfig.scopes, clonedConfig.scopes) + XCTAssertEqual(oidcClientConfig.redirectUri, clonedConfig.redirectUri) + XCTAssertEqual(oidcClientConfig.loginHint, clonedConfig.loginHint) + XCTAssertEqual(oidcClientConfig.nonce, clonedConfig.nonce) + XCTAssertEqual(oidcClientConfig.display, clonedConfig.display) + XCTAssertEqual(oidcClientConfig.prompt, clonedConfig.prompt) + XCTAssertEqual(oidcClientConfig.uiLocales, clonedConfig.uiLocales) + XCTAssertEqual(oidcClientConfig.acrValues, clonedConfig.acrValues) + XCTAssertEqual(oidcClientConfig.additionalParameters, clonedConfig.additionalParameters) + XCTAssertEqual(oidcClientConfig.httpClient.debugDescription, clonedConfig.httpClient.debugDescription) + } + + // TestRailCase(24719) + func testUpdate() { + let otherConfig = OidcClientConfig() + otherConfig.agent = AgentDelegate(agent: MockAgent(), agentConfig: (), oidcClientConfig: oidcClientConfig) + otherConfig.logger = LogManager.standard + otherConfig.storage = MockStorage() + otherConfig.discoveryEndpoint = "https://example.com" + otherConfig.clientId = "clientId" + otherConfig.scopes.insert("openid") + otherConfig.redirectUri = "http://localhost/callback" + otherConfig.loginHint = "loginHint" + otherConfig.nonce = "nonce" + otherConfig.display = "display" + otherConfig.prompt = "prompt" + otherConfig.uiLocales = "uiLocales" + otherConfig.acrValues = "acrValues" + otherConfig.additionalParameters = ["param": "value"] + otherConfig.httpClient = HttpClient() + + oidcClientConfig.update(with: otherConfig) + + XCTAssertEqual(otherConfig.openId.debugDescription, oidcClientConfig.openId.debugDescription) + XCTAssertEqual(otherConfig.agent.debugDescription, oidcClientConfig.agent.debugDescription) + XCTAssertEqual(otherConfig.discoveryEndpoint, oidcClientConfig.discoveryEndpoint) + XCTAssertEqual(otherConfig.clientId, oidcClientConfig.clientId) + XCTAssertEqual(otherConfig.scopes, oidcClientConfig.scopes) + XCTAssertEqual(otherConfig.redirectUri, oidcClientConfig.redirectUri) + XCTAssertEqual(otherConfig.loginHint, oidcClientConfig.loginHint) + XCTAssertEqual(otherConfig.nonce, oidcClientConfig.nonce) + XCTAssertEqual(otherConfig.display, oidcClientConfig.display) + XCTAssertEqual(otherConfig.prompt, oidcClientConfig.prompt) + XCTAssertEqual(otherConfig.uiLocales, oidcClientConfig.uiLocales) + XCTAssertEqual(otherConfig.acrValues, oidcClientConfig.acrValues) + XCTAssertEqual(otherConfig.additionalParameters, oidcClientConfig.additionalParameters) + XCTAssertEqual(otherConfig.httpClient.debugDescription, oidcClientConfig.httpClient.debugDescription) + } +} + +// Mock classes for AgentDelegateProtocol, Agent, HttpClient, etc. +class MockAgent: Agent { + func config() -> () -> T { + return {} + } + + func endSession(oidcConfig: PingOidc.OidcConfig, idToken: String) async throws -> Bool { + let params = [ + "client_id": oidcConfig.oidcClientConfig.clientId, + "id_token_hint": idToken + ] + let request = Request() + request.url(MockAPIEndpoint.endSession.url.absoluteString) + request.form(formData: params) + do { + let (_, _) = try await oidcConfig.oidcClientConfig.httpClient!.sendRequest(request: request) + } catch { + } + return true + } + + func authorize(oidcConfig: PingOidc.OidcConfig) async throws -> PingOidc.AuthCode { + return AuthCode(code: "TestAgent", codeVerifier: "codeVerifier") + } + + typealias T = Void +} diff --git a/Oidc/OidcTests/OidcClientTests.swift b/Oidc/OidcTests/OidcClientTests.swift new file mode 100644 index 0000000..a5a0006 --- /dev/null +++ b/Oidc/OidcTests/OidcClientTests.swift @@ -0,0 +1,319 @@ +// +// OidcClientTests.swift +// OidcTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOidc +@testable import PingOrchestrate +@testable import PingStorage + +final class OidcClientTests: XCTestCase { + var oidcClientConfig: OidcClientConfig! + var oidcClient: OidcClient! + + override func setUp() { + super.setUp() + oidcClientConfig = OidcClientConfig() + let agent = MockAgent() + oidcClientConfig.updateAgent(agent) + oidcClientConfig.discoveryEndpoint = MockAPIEndpoint.discovery.url.absoluteString + oidcClientConfig.storage = MockStorage() + oidcClientConfig.clientId = "test-client-id" + oidcClientConfig.httpClient = HttpClient(session: .shared) + oidcClient = OidcClient(config: oidcClientConfig) + + MockURLProtocol.startInterceptingRequests() + + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration) + case MockAPIEndpoint.token.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.token) + case MockAPIEndpoint.userinfo.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.userinfo.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.userinfo) + case MockAPIEndpoint.revocation.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.revocation.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, Data()) + case MockAPIEndpoint.endSession.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.endSession.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, Data()) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + } + + override func tearDown() { + MockURLProtocol.stopInterceptingRequests() + + oidcClient = nil + oidcClientConfig = nil + super.tearDown() + } + + // TestRailCase(22118) + func testFailedToLookupDiscoveryEndpoint() async throws { + + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: MockResponse.headers)!, Data()) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let result = await oidcClient.token() + + switch result { + case .success(_): + XCTFail("Should have failed with error") + case .failure(let failure): + switch failure { + case .apiError(let code, _): + XCTAssertEqual(code, 500) + case .authorizeError, .networkError, .unknown: + XCTFail("Should have failed with .apiError") + } + } + } + + // TestRailCase(22085) + func testAccessTokenShouldReturnCachedTokenIfNotExpired() async throws { + let result = await oidcClient.token() + switch result { + case .success( _): + break + case .failure(_): + XCTFail("Should have succeeded") + } + let cached = await oidcClient.token() + switch cached { + case .success( let token): + XCTAssertEqual(token.accessToken, "Dummy AccessToken") + XCTAssertEqual(token.tokenType, "Dummy Token Type") + XCTAssertEqual(token.refreshToken, "Dummy RefreshToken") + XCTAssertEqual(token.idToken, "Dummy IdToken") + XCTAssertEqual(token.scope, "openid email address") + break + case .failure(let error): + XCTFail("Should have succeeded, but failed with error \(error.errorMessage)") + } + + XCTAssertEqual(MockURLProtocol.requestHistory.count, 2) + } + + // TestRailCase(22086) + func testAccessTokenShouldRefreshTokenIfExpired() async throws { + let result = await oidcClient.token() + switch result { + case .success( _): + break + case .failure(_): + XCTFail("Should have succeeded") + } + + // Advance time by 1 second + try await Task.sleep(nanoseconds: 2_000_000_000) + + _ = await oidcClient.token() + + // auto refresh has been triggered + XCTAssertEqual(MockURLProtocol.requestHistory.count, 3) + XCTAssertEqual(Int(MockURLProtocol.requestHistory.last!.value(forHTTPHeaderField: "Content-Length")!), "grant_type=refresh_token&refresh_token=Dummy RefreshToken&client_id=test-client-id".count) + } + + // TestRailCase(24712) + func testRevokeShouldDeleteTokenFromStorage() async throws { + // First, get an access token + let result = await oidcClient.token() + switch result { + case .success( _): + break + case .failure(_): + XCTFail("Should have succeeded") + } + + // Then, revoke the access token + await oidcClient.revoke() + + // Check that the token is no longer in storage + let tokenInStorage = try await oidcClientConfig.storage.get() + XCTAssertNil(tokenInStorage) + } + + // TestRailCase(22087) + func testUserinfoShouldReturnUserInfo() async throws { + let result = await oidcClient.userinfo() + switch result { + case .success(let userinfo): + XCTAssertEqual(userinfo["sub"] as? String, "test-sub") + XCTAssertEqual(userinfo["name"] as? String, "test-name") + case .failure(_): + XCTFail("Should have succeeded") + } + } + + // TestRailCase(22088) + func testEndSessionShouldEndSessionAndRevokeToken() async throws { + // First, get an access token + let result = await oidcClient.token() + switch result { + case .success( _): + break + case .failure(_): + XCTFail("Should have succeeded") + } + + // Then, end the session + let endSessionResult = await oidcClient.endSession() + XCTAssertTrue(endSessionResult) + + // Check that the token is no longer in storage + let tokenInStorage = try await oidcClientConfig.storage.get() + XCTAssertNil(tokenInStorage) + + let revokeCalled = MockURLProtocol.requestHistory.contains(where: { request in + request.url?.path == MockAPIEndpoint.revocation.url.path + }) + + XCTAssertTrue(revokeCalled, "The /revoke endpoint was not called.") + + let signOffCalled = MockURLProtocol.requestHistory.contains(where: { request in + request.url?.path == MockAPIEndpoint.endSession.url.path + }) + XCTAssertTrue(signOffCalled, "The /signoff endpoint was not called.") + } + + // TestRailCase(22091) + func testFailedToRetrieveAccessToken() async throws { + + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration) + case MockAPIEndpoint.token.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenErrorResponse) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let result = await oidcClient.token() + switch result { + case .success(_): + XCTFail("Should have failed with error") + case .failure(let failure): + switch failure { + case .apiError(let code, _): + XCTAssertEqual(code, 400) + case .authorizeError, .networkError, .unknown: + XCTFail("Should have failed with .apiError(400)") + } + } + } + + // TestRailCase(22092) + func testFailedToInjectAccessTokenToUserinfo() async throws { + + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration) + case MockAPIEndpoint.token.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenErrorResponse) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let result = await oidcClient.userinfo() + switch result { + case .success(_): + XCTFail("Should have failed with error") + case .failure(let failure): + switch failure { + case .apiError(let code, _): + XCTAssertEqual(code, 400) + case .authorizeError, .networkError, .unknown: + XCTFail("Should have failed with .apiError(400)") + } + } + } + + // TestRailCase(22093) + func testFailedToRetrieveUserinfo() async throws { + + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration) + case MockAPIEndpoint.token.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.token) + case MockAPIEndpoint.userinfo.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.userinfo.url, statusCode: 401, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenErrorResponse) + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let result = await oidcClient.userinfo() + switch result { + case .success(_): + XCTFail("Should have failed with error") + case .failure(let failure): + switch failure { + case .apiError(let code, _): + XCTAssertEqual(code, 401) + case .authorizeError, .networkError, .unknown: + XCTFail("Should have failed with .apiError(401)") + } + } + } + + // TestRailCase(22094) + func testFailedToRefreshTokenAfterTokenExpired() async throws { + + MockURLProtocol.requestHandler = { request in + switch request.url!.path { + case MockAPIEndpoint.discovery.url.path: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration) + case MockAPIEndpoint.token.url.path: + // as httpBody is not available her (it is nil), we will check the `Content-Length` header value to see if the `grant_type` is `refresh_token' + if Int(request.value(forHTTPHeaderField: "Content-Length")!) == "grant_type=refresh_token&refresh_token=Dummy RefreshToken&client_id=test-client-id".count { + return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenErrorResponse) + } else { + return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.token) + } + default: + return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data()) + } + } + + let result = await oidcClient.token() + switch result { + case .success( _): break + case .failure( _): XCTFail("Should have succeeded") + } + + try await Task.sleep(nanoseconds: 2_000_000_000) + + let refreshResult = await oidcClient.token() + switch refreshResult { + case .success( _): break + case .failure( _): XCTFail("Should have succeeded") + } + + let revokeCalled = MockURLProtocol.requestHistory.contains(where: { request in + request.url?.path == "/revoke" + }) + + XCTAssertTrue(revokeCalled, "The /revoke endpoint was not called.") + } +} diff --git a/Oidc/OidcTests/PkceTests.swift b/Oidc/OidcTests/PkceTests.swift new file mode 100644 index 0000000..1bc6821 --- /dev/null +++ b/Oidc/OidcTests/PkceTests.swift @@ -0,0 +1,34 @@ +// +// PkceTests.swift +// OidcTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOidc + +final class PkceTests: XCTestCase { + + // TestRailCase(22111) + func testGeneratePkce() { + let pkce = Pkce.generate() + + XCTAssertFalse(pkce.codeVerifier.isEmpty, "Code verifier should not be empty") + XCTAssertFalse(pkce.codeChallenge.isEmpty, "Code challenge should not be empty") + XCTAssertEqual(pkce.codeChallengeMethod, "S256", "Code challenge method should be 'S256'") + } + + // TestRailCase(22110) + func testGenerateDifferentPkce() { + let pkce1 = Pkce.generate() + let pkce2 = Pkce.generate() + + XCTAssertTrue(pkce1.codeVerifier != pkce2.codeVerifier, "Code verifier should be different") + XCTAssertTrue(pkce1.codeChallenge != pkce2.codeChallenge, "Code challenge should be different") + } +} diff --git a/Oidc/OidcTests/TokenTests.swift b/Oidc/OidcTests/TokenTests.swift new file mode 100644 index 0000000..c7696af --- /dev/null +++ b/Oidc/OidcTests/TokenTests.swift @@ -0,0 +1,90 @@ +// +// TokenTests.swift +// OidcTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOidc + +final class TokenTests: XCTestCase { + + func testInitialization() { + let token = Token( + accessToken: "testAccessToken", + tokenType: "Bearer", + scope: "testScope", + expiresIn: 3600, + refreshToken: "testRefreshToken", + idToken: "testIdToken" + ) + + XCTAssertEqual(token.accessToken, "testAccessToken") + XCTAssertEqual(token.tokenType, "Bearer") + XCTAssertEqual(token.scope, "testScope") + XCTAssertEqual(token.expiresIn, 3600) + XCTAssertEqual(token.refreshToken, "testRefreshToken") + XCTAssertEqual(token.idToken, "testIdToken") + XCTAssertFalse(token.isExpired) + } + + // TestRailCase(22116, 22117) + func testEncodingDecoding() throws { + let token = Token( + accessToken: "testAccessToken", + tokenType: "Bearer", + scope: "testScope", + expiresIn: 3600, + refreshToken: "testRefreshToken", + idToken: "testIdToken" + ) + + let encoder = JSONEncoder() + let decoder = JSONDecoder() + + let data = try encoder.encode(token) + let decodedToken = try decoder.decode(Token.self, from: data) + + XCTAssertEqual(decodedToken.accessToken, "testAccessToken") + XCTAssertEqual(decodedToken.tokenType, "Bearer") + XCTAssertEqual(decodedToken.scope, "testScope") + XCTAssertEqual(decodedToken.expiresIn, 3600) + XCTAssertEqual(decodedToken.refreshToken, "testRefreshToken") + XCTAssertEqual(decodedToken.idToken, "testIdToken") + XCTAssertEqual(decodedToken.expiresAt, token.expiresAt) + } + + // TestRailCase(22112) + func testIsExpired() { + let token = Token( + accessToken: "testAccessToken", + tokenType: "Bearer", + scope: "testScope", + expiresIn: -1, + refreshToken: "testRefreshToken", + idToken: "testIdToken" + ) + + XCTAssertTrue(token.isExpired) + } + + // TestRailCase(22114, 22115) + func testIsExpiredWithThreshold() { + let token = Token( + accessToken: "testAccessToken", + tokenType: "Bearer", + scope: "testScope", + expiresIn: 3600, + refreshToken: "testRefreshToken", + idToken: "testIdToken" + ) + + XCTAssertTrue(token.isExpired(threshold: 3601)) + XCTAssertFalse(token.isExpired(threshold: 3599)) + } +} diff --git a/Oidc/OidcTests/mock/MockAPIEndpoint.swift b/Oidc/OidcTests/mock/MockAPIEndpoint.swift new file mode 100644 index 0000000..55cd52c --- /dev/null +++ b/Oidc/OidcTests/mock/MockAPIEndpoint.swift @@ -0,0 +1,40 @@ +// +// MockAPIEndpoint.swift +// OidcTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +enum MockAPIEndpoint { + static let baseURL = "https://auth.test-one-pingone.com" + + case authorization + case token + case userinfo + case endSession + case revocation + case discovery + + var url: URL { + switch self { + case .authorization: + return URL(string: "\(MockAPIEndpoint.baseURL)/authorize")! + case .token: + return URL(string: "\(MockAPIEndpoint.baseURL)/token")! + case .userinfo: + return URL(string: "\(MockAPIEndpoint.baseURL)/userinfo")! + case .endSession: + return URL(string: "\(MockAPIEndpoint.baseURL)/signoff")! + case .revocation: + return URL(string: "\(MockAPIEndpoint.baseURL)/revoke")! + case .discovery: + return URL(string: "\(MockAPIEndpoint.baseURL)/.well-known/openid-configuration")! + } + } +} diff --git a/Oidc/OidcTests/mock/MockResponse.swift b/Oidc/OidcTests/mock/MockResponse.swift new file mode 100644 index 0000000..dd13dce --- /dev/null +++ b/Oidc/OidcTests/mock/MockResponse.swift @@ -0,0 +1,69 @@ +// +// MockResponse.swift +// OidcTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +struct MockResponse { + static let headers = ["Content-Type": "application/json"] + + static var openIdConfiguration: Data { + """ + { + "authorization_endpoint" : "\(MockAPIEndpoint.authorization.url.absoluteString)", + "token_endpoint" : "\(MockAPIEndpoint.token.url.absoluteString)", + "userinfo_endpoint" : "\(MockAPIEndpoint.userinfo.url.absoluteString)", + "end_session_endpoint" : "\(MockAPIEndpoint.endSession.url.absoluteString)", + "revocation_endpoint" : "\(MockAPIEndpoint.revocation.url.absoluteString)" + } + """.data(using: .utf8)! + } + + static var token: Data { + """ + { + "access_token" : "Dummy AccessToken", + "token_type" : "Dummy Token Type", + "scope" : "openid email address", + "refresh_token" : "Dummy RefreshToken", + "expires_in" : 2, + "id_token" : "Dummy IdToken" + } + """.data(using: .utf8)! + } + + static var userinfo: Data { + """ + { + "sub" : "test-sub", + "name" : "test-name", + "email" : "test-email", + "phone_number" : "test-phone_number", + "address" : "test-address" + } + """.data(using: .utf8)! + } + + static var tokenErrorResponse: Data { + """ + { + "error" : "Invalid Grant" + } + """.data(using: .utf8)! + } + + static var error: Data { + """ + { + "error" : "Internal Server Error" + } + """.data(using: .utf8)! + } +} diff --git a/Oidc/OidcTests/mock/MockStorage.swift b/Oidc/OidcTests/mock/MockStorage.swift new file mode 100644 index 0000000..9fb6eee --- /dev/null +++ b/Oidc/OidcTests/mock/MockStorage.swift @@ -0,0 +1,36 @@ +// +// MockStorage.swift +// OidcTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingStorage + +public class Mock: Storage { + private var data: T? + + public func save(item: T) async throws { + data = item + } + + public func get() async throws -> T? { + return data + } + + public func delete() async throws { + data = nil + } +} + +public class MockStorage: StorageDelegate { + public init(cacheable: Bool = false) { + super.init(delegate: Mock(), cacheable: cacheable) + } +} + diff --git a/Oidc/OidcTests/mock/MockURLProtocol.swift b/Oidc/OidcTests/mock/MockURLProtocol.swift new file mode 100644 index 0000000..984cfa4 --- /dev/null +++ b/Oidc/OidcTests/mock/MockURLProtocol.swift @@ -0,0 +1,58 @@ +// +// MockURLProtocol.swift +// OidcTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +import PingLogger + +class MockURLProtocol: URLProtocol { + public static var requestHistory: [URLRequest] = [URLRequest]() + + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + static func startInterceptingRequests() { + URLProtocol.registerClass(MockURLProtocol.self) + } + + static func stopInterceptingRequests() { + URLProtocol.unregisterClass(MockURLProtocol.self) + requestHistory.removeAll() + } + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + MockURLProtocol.requestHistory.append(request) + + guard let handler = MockURLProtocol.requestHandler else { + XCTFail("Received unexpected request with no handler set") + return + } + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() { + + } +} diff --git a/Oidc/README.md b/Oidc/README.md new file mode 100644 index 0000000..4f12c92 --- /dev/null +++ b/Oidc/README.md @@ -0,0 +1,132 @@ +

+ + Logo + +


+

+ +`PingOidc` module provides OIDC client for PingOne and ForgeRock platform. + +The `PingOidc` module follows the [OIDC](https://openid.net/specs/openid-connect-core-1_0.html) specification and +provides a simple and easy-to-use API to interact with the OIDC server. It allows you to authenticate, retrieve the +access token, revoke the token, and sign out from the OIDC server. + +## Integrating the SDK into your project + +Use Cocoapods or Swift Package Manger + +## Oidc Client Configuration + +Basic Configuration, use `discoveryEndpoint` to lookup OIDC endpoints + +```swift +let config = OidcClientConfig() +config.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration" +config.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1" +config.redirectUri = "org.forgerock.demo://oauth2redirect" +config.scopes = ["openid", "email", "address", "profile", "phone"] + +let ping = OidcClient(config: config) + +let result = await ping.token() // Retrieve the access token +switch result { +case .success(let token): + let accessToken = token +case .failure(let error): + switch error { + case .apiError: + //Address error + break + case .authorizeError: + //Address error + break + case .networkError: + //Address error + break + case .unknown: + //Address error + break + } +} + +await ping.revoke() //Revoke the access token +_ = await ping.endSession() //End the session +``` + +By default, the SDK uses `KeychainStorage` (with `SecuredKeyEncryptor` ) to store the token and `none` Logger is set, +however developers can override the storage and logger settings. + +Basic Configuration with custom `storage` and `logger` + +```swift +let config = OidcClientConfig() +config.logger = LogManager.standard //Log to console +config.storage = CustomStorage() //Use Custom Storage +//... + +let ping = OidcClient(config: config) +``` + +More OidcClient configuration, configurable attribute can be found under +[OIDC Spec](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) + +```swift +let config = OidcClientConfig() +config.acrValues = "urn:acr:form" +config.loginHint = "test" +config.display = "test" +//... + +let ping = OidcClient(config: config) +``` + +## Custom Agent + +You can also provide a custom agent to launch the authorization request. +You can implement the `Agent` interface to create a custom agent. + +```swift +protocol Agent { + associatedtype T + + func config() -> () -> T + func endSession(oidcConfig: OidcConfig, idToken: String) async throws -> Bool + func authorize(oidcConfig: OidcConfig) async throws -> AuthCode +} +``` + +Here is an example of creating a custom agent. + +```swift +//Create a custom agent configuration +struct CustomAgentConfig { + var config1 = "config1Value" + var config2 = "config2Value" +} + +class CustomAgent: Agent { + func config() -> () -> CustomAgentConfig { + return { CustomAgentConfig() } + } + + func authorize(oidcConfig: Oidc.OidcConfig) async throws -> Oidc.AuthCode { + oidcConfig.config.config2 //Access the agent configuration + oidcConfig.oidcClientConfig.openId?.endSessionEndpoint //Access the oidcClientConfig + return AuthCode(code: "TestAgent", codeVerifier: "") + } + + func endSession(oidcConfig: Oidc.OidcConfig, idToken: String) async throws -> Bool { + //Logout session with idToken + oidcConfig.config.config1 //Access the agent configuration + oidcConfig.oidcClientConfig.openId?.endSessionEndpoint //Access the oidcClientConfig + return true + } +} + +let config = OidcClientConfig() +config.updateAgent(CustomAgent()) +//... + +let ping = OidcClient(config: config) + +``` diff --git a/Orchestrate/Orchestrate.xcodeproj/project.pbxproj b/Orchestrate/Orchestrate.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bfa9e32 --- /dev/null +++ b/Orchestrate/Orchestrate.xcodeproj/project.pbxproj @@ -0,0 +1,628 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 63; + objects = { + +/* Begin PBXBuildFile section */ + 3A12F9BE2BCE20B50087DF67 /* Workflow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A12F9BD2BCE20B50087DF67 /* Workflow.swift */; }; + 3A203D592BD9DC600020C995 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D582BD9DC600020C995 /* Request.swift */; }; + 3A203D5B2BD9E2C80020C995 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D5A2BD9E2C80020C995 /* Node.swift */; }; + 3A203D772BDA1CEC0020C995 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 3A203D762BDA1CEC0020C995 /* README.md */; }; + 3A203DA22BE0312A0020C995 /* HttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA12BE0312A0020C995 /* HttpClient.swift */; }; + 3A203DA42BE068BE0020C995 /* WorkflowConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA32BE068BE0020C995 /* WorkflowConfig.swift */; }; + 3A203DA62BE068E10020C995 /* ModuleRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA52BE068E10020C995 /* ModuleRegistry.swift */; }; + 3A203DA82BE069120020C995 /* Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA72BE069120020C995 /* Setup.swift */; }; + 3A203DAA2BE06DFF0020C995 /* CookieModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA92BE06DFF0020C995 /* CookieModule.swift */; }; + 3A2E86902C179E2F000EC6E2 /* FlowContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2E868F2C179E2F000EC6E2 /* FlowContextTests.swift */; }; + 3A54417F2BCDF1D900385131 /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */; }; + 3A5441842BCDF1D900385131 /* OrchestrateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441832BCDF1D900385131 /* OrchestrateTests.swift */; }; + 3A5441852BCDF1D900385131 /* Orchestrate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A5441772BCDF1D900385131 /* Orchestrate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3A7575762C063F2A00891EC7 /* ModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575752C063F2A00891EC7 /* ModuleTests.swift */; }; + 3A7575782C0673A100891EC7 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575772C0673A100891EC7 /* ResponseTests.swift */; }; + 3A75757A2C06947000891EC7 /* SharedContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575792C06947000891EC7 /* SharedContext.swift */; }; + 3A75757C2C06B59F00891EC7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A75757B2C06B59F00891EC7 /* RequestTests.swift */; }; + 3AB1C9E92BD6410A003FCE3C /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1C9E52BD6410A003FCE3C /* Response.swift */; }; + 3AB1C9F22BD6B2F9003FCE3C /* CustomHTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1C9F02BD6B2F9003FCE3C /* CustomHTTPCookie.swift */; }; + 3AF7D3BB2BF3CF410056F497 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF7D3BA2BF3CF410056F497 /* Module.swift */; }; + A50981B92CEBDF0800F4B487 /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A50981B72CEBDF0800F4B487 /* PingLogger.framework */; }; + A50981BA2CEBDF0800F4B487 /* PingLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A50981B72CEBDF0800F4B487 /* PingLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A50981BB2CEBDF0800F4B487 /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A50981B82CEBDF0800F4B487 /* PingStorage.framework */; }; + A50981BC2CEBDF0800F4B487 /* PingStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A50981B82CEBDF0800F4B487 /* PingStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A51D4CE32C62EB4B00FE09E0 /* CustomHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CE22C62EB4B00FE09E0 /* CustomHeader.swift */; }; + A51D4CFC2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CFB2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift */; }; + A51D4CFE2C6BAAE900FE09E0 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CFD2C6BAAE900FE09E0 /* MockURLProtocol.swift */; }; + A51D4D002C6BABEF00FE09E0 /* MockAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CFF2C6BABEF00FE09E0 /* MockAPIEndpoint.swift */; }; + A51D4D272C6C1B2800FE09E0 /* NodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4D262C6C1B2800FE09E0 /* NodeTests.swift */; }; + A51D4D292C6C1EA700FE09E0 /* CookieModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4D282C6C1EA700FE09E0 /* CookieModuleTests.swift */; }; + A51D4D2B2C6C222600FE09E0 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4D2A2C6C222600FE09E0 /* SessionTests.swift */; }; + A51D4D2D2C6C260400FE09E0 /* WorkflowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4D2C2C6C260400FE09E0 /* WorkflowTests.swift */; }; + A5A712462CAC523B00B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A712452CAC523B00B7DD58 /* PrivacyInfo.xcprivacy */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3A5441802BCDF1D900385131 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A54416B2BCDF1D900385131 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A5441732BCDF1D900385131; + remoteInfo = PingOrchestrate; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 3AB7D7CE2C012C0000CA4A02 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + A50981BC2CEBDF0800F4B487 /* PingStorage.framework in Embed Frameworks */, + A50981BA2CEBDF0800F4B487 /* PingLogger.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3A12F9BD2BCE20B50087DF67 /* Workflow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Workflow.swift; sourceTree = ""; }; + 3A203D582BD9DC600020C995 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + 3A203D5A2BD9E2C80020C995 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; + 3A203D762BDA1CEC0020C995 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 3A203DA12BE0312A0020C995 /* HttpClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpClient.swift; sourceTree = ""; }; + 3A203DA32BE068BE0020C995 /* WorkflowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowConfig.swift; sourceTree = ""; }; + 3A203DA52BE068E10020C995 /* ModuleRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleRegistry.swift; sourceTree = ""; }; + 3A203DA72BE069120020C995 /* Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setup.swift; sourceTree = ""; }; + 3A203DA92BE06DFF0020C995 /* CookieModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieModule.swift; sourceTree = ""; }; + 3A2E868F2C179E2F000EC6E2 /* FlowContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowContextTests.swift; sourceTree = ""; }; + 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingOrchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A5441772BCDF1D900385131 /* Orchestrate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Orchestrate.h; sourceTree = ""; }; + 3A54417E2BCDF1D900385131 /* OrchestrateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OrchestrateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A5441832BCDF1D900385131 /* OrchestrateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrchestrateTests.swift; sourceTree = ""; }; + 3A7575752C063F2A00891EC7 /* ModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleTests.swift; sourceTree = ""; }; + 3A7575772C0673A100891EC7 /* ResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseTests.swift; sourceTree = ""; }; + 3A7575792C06947000891EC7 /* SharedContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedContext.swift; sourceTree = ""; }; + 3A75757B2C06B59F00891EC7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; }; + 3AB1C9E52BD6410A003FCE3C /* Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; + 3AB1C9F02BD6B2F9003FCE3C /* CustomHTTPCookie.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomHTTPCookie.swift; sourceTree = ""; }; + 3AF7D3BA2BF3CF410056F497 /* Module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; }; + A50981B72CEBDF0800F4B487 /* PingLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A50981B82CEBDF0800F4B487 /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A51D4CE22C62EB4B00FE09E0 /* CustomHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHeader.swift; sourceTree = ""; }; + A51D4CFB2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHeaderModuleTests.swift; sourceTree = ""; }; + A51D4CFD2C6BAAE900FE09E0 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; + A51D4CFF2C6BABEF00FE09E0 /* MockAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIEndpoint.swift; sourceTree = ""; }; + A51D4D262C6C1B2800FE09E0 /* NodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeTests.swift; sourceTree = ""; }; + A51D4D282C6C1EA700FE09E0 /* CookieModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieModuleTests.swift; sourceTree = ""; }; + A51D4D2A2C6C222600FE09E0 /* SessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; + A51D4D2C2C6C260400FE09E0 /* WorkflowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowTests.swift; sourceTree = ""; }; + A5A712452CAC523B00B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3A5441712BCDF1D900385131 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A50981BB2CEBDF0800F4B487 /* PingStorage.framework in Frameworks */, + A50981B92CEBDF0800F4B487 /* PingLogger.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A54417B2BCDF1D900385131 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A54417F2BCDF1D900385131 /* PingOrchestrate.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3A54416A2BCDF1D900385131 = { + isa = PBXGroup; + children = ( + 3A203D762BDA1CEC0020C995 /* README.md */, + 3A5441762BCDF1D900385131 /* Orchestrate */, + 3A5441822BCDF1D900385131 /* OrchestrateTests */, + 3A5441752BCDF1D900385131 /* Products */, + 3AB7D7CA2C012C0000CA4A02 /* Frameworks */, + ); + sourceTree = ""; + }; + 3A5441752BCDF1D900385131 /* Products */ = { + isa = PBXGroup; + children = ( + 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */, + 3A54417E2BCDF1D900385131 /* OrchestrateTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 3A5441762BCDF1D900385131 /* Orchestrate */ = { + isa = PBXGroup; + children = ( + A5A712452CAC523B00B7DD58 /* PrivacyInfo.xcprivacy */, + 3A5441772BCDF1D900385131 /* Orchestrate.h */, + 3A203DA92BE06DFF0020C995 /* CookieModule.swift */, + A51D4CE22C62EB4B00FE09E0 /* CustomHeader.swift */, + 3A203DA12BE0312A0020C995 /* HttpClient.swift */, + 3AF7D3BA2BF3CF410056F497 /* Module.swift */, + 3A203DA52BE068E10020C995 /* ModuleRegistry.swift */, + 3A203D5A2BD9E2C80020C995 /* Node.swift */, + 3AB1C9F02BD6B2F9003FCE3C /* CustomHTTPCookie.swift */, + 3A203D582BD9DC600020C995 /* Request.swift */, + 3AB1C9E52BD6410A003FCE3C /* Response.swift */, + 3A203DA72BE069120020C995 /* Setup.swift */, + 3A7575792C06947000891EC7 /* SharedContext.swift */, + 3A12F9BD2BCE20B50087DF67 /* Workflow.swift */, + 3A203DA32BE068BE0020C995 /* WorkflowConfig.swift */, + ); + path = Orchestrate; + sourceTree = ""; + }; + 3A5441822BCDF1D900385131 /* OrchestrateTests */ = { + isa = PBXGroup; + children = ( + A509D1CB2C6D03AC003A0006 /* mock */, + A51D4D282C6C1EA700FE09E0 /* CookieModuleTests.swift */, + A51D4CFB2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift */, + 3A2E868F2C179E2F000EC6E2 /* FlowContextTests.swift */, + 3A7575752C063F2A00891EC7 /* ModuleTests.swift */, + A51D4D262C6C1B2800FE09E0 /* NodeTests.swift */, + 3A5441832BCDF1D900385131 /* OrchestrateTests.swift */, + 3A75757B2C06B59F00891EC7 /* RequestTests.swift */, + 3A7575772C0673A100891EC7 /* ResponseTests.swift */, + A51D4D2A2C6C222600FE09E0 /* SessionTests.swift */, + A51D4D2C2C6C260400FE09E0 /* WorkflowTests.swift */, + ); + path = OrchestrateTests; + sourceTree = ""; + }; + 3AB7D7CA2C012C0000CA4A02 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A50981B72CEBDF0800F4B487 /* PingLogger.framework */, + A50981B82CEBDF0800F4B487 /* PingStorage.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + A509D1CB2C6D03AC003A0006 /* mock */ = { + isa = PBXGroup; + children = ( + A51D4CFD2C6BAAE900FE09E0 /* MockURLProtocol.swift */, + A51D4CFF2C6BABEF00FE09E0 /* MockAPIEndpoint.swift */, + ); + path = mock; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3A54416F2BCDF1D900385131 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A5441852BCDF1D900385131 /* Orchestrate.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3A5441732BCDF1D900385131 /* PingOrchestrate */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A5441882BCDF1D900385131 /* Build configuration list for PBXNativeTarget "PingOrchestrate" */; + buildPhases = ( + 3A54416F2BCDF1D900385131 /* Headers */, + 3A5441702BCDF1D900385131 /* Sources */, + 3A5441712BCDF1D900385131 /* Frameworks */, + 3A5441722BCDF1D900385131 /* Resources */, + 3AB7D7CE2C012C0000CA4A02 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PingOrchestrate; + productName = PingOrchestrate; + productReference = 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */; + productType = "com.apple.product-type.framework"; + }; + 3A54417D2BCDF1D900385131 /* OrchestrateTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A54418B2BCDF1D900385131 /* Build configuration list for PBXNativeTarget "OrchestrateTests" */; + buildPhases = ( + 3A54417A2BCDF1D900385131 /* Sources */, + 3A54417B2BCDF1D900385131 /* Frameworks */, + 3A54417C2BCDF1D900385131 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3A5441812BCDF1D900385131 /* PBXTargetDependency */, + ); + name = OrchestrateTests; + productName = PingOrchestrateTests; + productReference = 3A54417E2BCDF1D900385131 /* OrchestrateTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3A54416B2BCDF1D900385131 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1540; + TargetAttributes = { + 3A5441732BCDF1D900385131 = { + CreatedOnToolsVersion = 15.0; + }; + 3A54417D2BCDF1D900385131 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = 3A54416E2BCDF1D900385131 /* Build configuration list for PBXProject "Orchestrate" */; + compatibilityVersion = "Xcode 15.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3A54416A2BCDF1D900385131; + productRefGroup = 3A5441752BCDF1D900385131 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3A5441732BCDF1D900385131 /* PingOrchestrate */, + 3A54417D2BCDF1D900385131 /* OrchestrateTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3A5441722BCDF1D900385131 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A712462CAC523B00B7DD58 /* PrivacyInfo.xcprivacy in Resources */, + 3A203D772BDA1CEC0020C995 /* README.md in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A54417C2BCDF1D900385131 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3A5441702BCDF1D900385131 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A203DA62BE068E10020C995 /* ModuleRegistry.swift in Sources */, + 3A203DAA2BE06DFF0020C995 /* CookieModule.swift in Sources */, + 3A203DA42BE068BE0020C995 /* WorkflowConfig.swift in Sources */, + 3A203DA22BE0312A0020C995 /* HttpClient.swift in Sources */, + 3AF7D3BB2BF3CF410056F497 /* Module.swift in Sources */, + 3A203DA82BE069120020C995 /* Setup.swift in Sources */, + 3A203D5B2BD9E2C80020C995 /* Node.swift in Sources */, + 3A12F9BE2BCE20B50087DF67 /* Workflow.swift in Sources */, + 3AB1C9F22BD6B2F9003FCE3C /* CustomHTTPCookie.swift in Sources */, + 3A75757A2C06947000891EC7 /* SharedContext.swift in Sources */, + 3AB1C9E92BD6410A003FCE3C /* Response.swift in Sources */, + 3A203D592BD9DC600020C995 /* Request.swift in Sources */, + A51D4CE32C62EB4B00FE09E0 /* CustomHeader.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A54417A2BCDF1D900385131 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A51D4D2B2C6C222600FE09E0 /* SessionTests.swift in Sources */, + A51D4D002C6BABEF00FE09E0 /* MockAPIEndpoint.swift in Sources */, + A51D4D272C6C1B2800FE09E0 /* NodeTests.swift in Sources */, + 3A2E86902C179E2F000EC6E2 /* FlowContextTests.swift in Sources */, + 3A5441842BCDF1D900385131 /* OrchestrateTests.swift in Sources */, + 3A75757C2C06B59F00891EC7 /* RequestTests.swift in Sources */, + A51D4D2D2C6C260400FE09E0 /* WorkflowTests.swift in Sources */, + A51D4CFC2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift in Sources */, + 3A7575782C0673A100891EC7 /* ResponseTests.swift in Sources */, + 3A7575762C063F2A00891EC7 /* ModuleTests.swift in Sources */, + A51D4D292C6C1EA700FE09E0 /* CookieModuleTests.swift in Sources */, + A51D4CFE2C6BAAE900FE09E0 /* MockURLProtocol.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3A5441812BCDF1D900385131 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A5441732BCDF1D900385131 /* PingOrchestrate */; + targetProxy = 3A5441802BCDF1D900385131 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 3A5441862BCDF1D900385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 3A5441872BCDF1D900385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 3A5441892BCDF1D900385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Orchestrate; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3A54418A2BCDF1D900385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Orchestrate; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3A54418C2BCDF1D900385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.OrchestrateTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_STRICT_CONCURRENCY = minimal; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Debug; + }; + 3A54418D2BCDF1D900385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.OrchestrateTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_STRICT_CONCURRENCY = minimal; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3A54416E2BCDF1D900385131 /* Build configuration list for PBXProject "Orchestrate" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A5441862BCDF1D900385131 /* Debug */, + 3A5441872BCDF1D900385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A5441882BCDF1D900385131 /* Build configuration list for PBXNativeTarget "PingOrchestrate" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A5441892BCDF1D900385131 /* Debug */, + 3A54418A2BCDF1D900385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A54418B2BCDF1D900385131 /* Build configuration list for PBXNativeTarget "OrchestrateTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A54418C2BCDF1D900385131 /* Debug */, + 3A54418D2BCDF1D900385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3A54416B2BCDF1D900385131 /* Project object */; +} diff --git a/Orchestrate/Orchestrate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Orchestrate/Orchestrate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Orchestrate/Orchestrate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Orchestrate/Orchestrate.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Orchestrate/Orchestrate.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..16cf018 --- /dev/null +++ b/Orchestrate/Orchestrate.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,17 @@ + + + + + FILEHEADER + +// ___FILENAME___ +// ___PACKAGENAME___ +// +// Copyright (c) ___YEAR___ Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + + diff --git a/Orchestrate/Orchestrate.xcodeproj/xcshareddata/xcschemes/Orchestrate.xcscheme b/Orchestrate/Orchestrate.xcodeproj/xcshareddata/xcschemes/Orchestrate.xcscheme new file mode 100644 index 0000000..c5c6800 --- /dev/null +++ b/Orchestrate/Orchestrate.xcodeproj/xcshareddata/xcschemes/Orchestrate.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Orchestrate/Orchestrate/CookieModule.swift b/Orchestrate/Orchestrate/CookieModule.swift new file mode 100644 index 0000000..f085ea0 --- /dev/null +++ b/Orchestrate/Orchestrate/CookieModule.swift @@ -0,0 +1,301 @@ +// +// CookieModule.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingStorage + +/// A module that manages cookies. +public class CookieModule { + + /// Initializes a new instance of `CookieModule`. + public init() {} + + /// The module configuration for managing cookies. + public static let config: Module = Module.of({ CookieConfig() }) { + setup in + + setup.initialize { + setup.context.set(key: SharedContext.Keys.cookieStorage, value: setup.config.cookieStorage) + } + + setup.start { context, request in + let cookies = try? await setup.config.cookieStorage.get() + if let url = request.urlRequest.url, let cookies = cookies { + CookieModule.inject(url: url, + cookies: cookies, + inMemoryStorage: setup.config.inMemoryStorage, + request: request) + } + return request + } + + setup.next { context, _, request in + if let url = request.urlRequest.url { + let allCookies = setup.config.inMemoryStorage.cookies(for: url) + if let allCookies = allCookies { + request.cookies(cookies: allCookies) + } + if let cookies = try? await setup.config.cookieStorage.get() { + CookieModule.inject(url: url, cookies: cookies, inMemoryStorage: setup.config.inMemoryStorage, request: request) + } + } + return request + } + + setup.response { context, response in + let cookies = response.getCookies() + if cookies.count > 0, let url = response.response.url { + await CookieModule.parseResponseForCookie(context: context, + url: url, + cookies: cookies, + storage: setup.config.inMemoryStorage, + cookieConfig: setup.config) + } + } + + setup.signOff { request in + if let url = request.urlRequest.url { + if let cookies = try? await setup.config.cookieStorage.get() { + CookieModule.inject(url: url, cookies: cookies, inMemoryStorage: setup.config.inMemoryStorage, request: request) + } + try? await setup.config.cookieStorage.delete() + setup.config.inMemoryStorage.deleteCookies(url: url) + } + return request + } + } + + /// Injects cookies into an HTTP request. + /// - Parameters: + /// - url: The URL of the request. + /// - cookies: The cookies to be injected. + /// - inMemoryStorage: In-memory cookie storage. + /// - request: The HTTP request to modify. + static func inject(url: URL, + cookies: [CustomHTTPCookie], + inMemoryStorage: InMemoryCookieStorage?, + request: Request) { + + inMemoryStorage?.deleteCookies(url: url) + + cookies.compactMap { $0.toHTTPCookie() } + .forEach { inMemoryStorage?.setCookie($0) } + + if let cookie = inMemoryStorage?.cookies(for: url) { + request.cookies(cookies: cookie) + } + } + + /// Parses cookies from an HTTP response and updates storage. + /// - Parameters: + /// - context: The workflow context. + /// - url: The URL associated with the response. + /// - cookies: The cookies received in the response. + /// - storage: In-memory cookie storage. + /// - cookieConfig: Configuration for cookie persistence. + static func parseResponseForCookie(context: FlowContext, + url: URL, + cookies: [HTTPCookie], + storage: InMemoryCookieStorage?, + cookieConfig: CookieConfig) async { + + let persistCookies = cookies.filter { cookieConfig.persist.contains($0.name) } + let otherCookies = cookies.filter { !cookieConfig.persist.contains($0.name) } + + storage?.deleteCookies(url: url) + + if !persistCookies.isEmpty { + + // Add existing cookies to cookie storage + try? await cookieConfig.cookieStorage.get()?.compactMap { $0.toHTTPCookie() }.forEach { + storage?.setCookie($0) + } + + // Clear existing cookies from keychain + try? await cookieConfig.cookieStorage.delete() + + // Add new cookies to temp cookie storage + persistCookies.forEach { + storage?.setCookie($0) + } + + // Persist only the required cookies to keychain + let cookieData = storage?.cookies(for: url)? + .filter { cookieConfig.persist.contains($0.name) } + .compactMap { value in + CustomHTTPCookie(from: value) + } + if let cookieData = cookieData { + try? await cookieConfig.cookieStorage.save(item: cookieData) + } + + } + + // Persist non-persist cookies to cookie storage + otherCookies.forEach { storage?.setCookie($0) } + } +} + + +/// Configuration for managing cookies in the application. +public class CookieConfig { + typealias Cookies = [String] + + /// A list of Cookies name that should be persisted to the storage. For cookies that should not be persisted, do not add the cookie name to this list. + public var persist: [String] = [] + /// In-memory storage for cookies. + public private(set) var inMemoryStorage: InMemoryCookieStorage + /// Persistent storage for cookies. + public internal(set) var cookieStorage: StorageDelegate<[CustomHTTPCookie]> + + /// Initializes a new instance of `CookieConfig`. + public init() { + cookieStorage = KeychainStorage<[CustomHTTPCookie]>(account: SharedContext.Keys.cookieStorage, encryptor: SecuredKeyEncryptor() ?? NoEncryptor()) + inMemoryStorage = InMemoryCookieStorage() + } +} + + +extension Workflow { + /// Checks if the workflow has cookies available in storage. + /// - Returns: A Boolean value indicating whether cookies exist in the storage. + public func hasCookies() async -> Bool { + let storage = sharedContext.get(key: SharedContext.Keys.cookieStorage) as? StorageDelegate<[CustomHTTPCookie]> + let value = try? await storage?.get() + return (value != nil) && (value?.count ?? 0 > 0) + } +} + +/// A storage class for managing in-memory cookies. +public final class InMemoryCookieStorage: HTTPCookieStorage { + private var cookieStore: [HTTPCookie] = [] + + /// Adds or updates a cookie in the storage. + /// - Parameter cookie: The cookie to add or update. + public override func setCookie(_ cookie: HTTPCookie) { + cookieStore.removeAll { $0.name == cookie.name && $0.domain == cookie.domain && $0.path == cookie.path } + cookieStore.append(cookie) + } + + /// Deletes a specific cookie from the storage. + /// - Parameter cookie: The cookie to delete. + public override func deleteCookie(_ cookie: HTTPCookie) { + cookieStore.removeAll { $0 == cookie } + } + + /// Deletes all cookies associated with a specific URL. + /// - Parameter url: The URL whose cookies should be deleted. + public func deleteCookies(url: URL) { + cookies(for: url)?.forEach { value in + deleteCookie(value) + } + } + + /// Retrieves all cookies currently stored. + public override var cookies: [HTTPCookie]? { + return cookieStore + } + + /// Retrieves cookies associated with a specific URL. + /// - Parameter url: The URL to fetch cookies for. + public override func cookies(for url: URL) -> [HTTPCookie]? { + return cookieStore.filter {!$0.isExpired && $0.validateURL(url) } + } + + /// Deletes all cookies from the storage./// Adds multiple cookies to the storage. + /// - Parameters: + /// - cookies: The cookies to add. + /// - url: The URL associated with the cookies (optional). + /// - mainDocumentURL: The main document URL (optional). + public override func setCookies(_ cookies: [HTTPCookie], for url: URL?, mainDocumentURL: URL?) { + for cookie in cookies { + setCookie(cookie) + } + } +} + + +extension SharedContext.Keys { + static let cookieStorage = "COOKIE_STORAGE" +} + + +extension HTTPCookie { + var isExpired: Bool { + get { + if let expDate = self.expiresDate, expDate.timeIntervalSince1970 < Date().timeIntervalSince1970 { + return true + } + return false + } + } + + func validateIsSecure(_ url: URL) -> Bool { + if !self.isSecure { + return true + } + if let urlScheme = url.scheme, urlScheme.lowercased() == "https" { + return true + } + return false + } + + func validateURL(_ url: URL) -> Bool { + return self.validateDomain(url: url) && self.validatePath(url: url) + } + + private func validatePath(url: URL) -> Bool { + let path = url.path.count == 0 ? "/" : url.path + + // For exact matching i.e. /path == /path + if path == self.path { + return true + } + + // For partial matching + if path.hasPrefix(self.path) { + // if Cookie path ends with / + // i.e. /abc == / or /abc/def == /abc/ + if self.path.hasSuffix("/") { + return true + } + + // making sure to validate exact path matching + // i.e. /abcd != /abc, /abc/def == /abc + if path.hasPrefix(self.path + "/") { + return true + } + } + return false + } + + private func validateDomain(url: URL) -> Bool { + + guard let host = url.host else { + // Invalid URL host + return false + } + + // For exact matching i.e. forgerock.com == forgerock.com or am.forgerock.com == am.forgerock.com + if host == self.domain { + return true + } + // For sub domain matching i.e. demo.forgerock.com == .forgerock.com + if host.hasSuffix(self.domain) { + return true + } + // For ignoring leading dot + if (self.domain.count - host.count == 1) && self.domain.hasPrefix(".") { + return true + } + return false + } +} diff --git a/Orchestrate/Orchestrate/CustomHTTPCookie.swift b/Orchestrate/Orchestrate/CustomHTTPCookie.swift new file mode 100644 index 0000000..c67ebc5 --- /dev/null +++ b/Orchestrate/Orchestrate/CustomHTTPCookie.swift @@ -0,0 +1,92 @@ +// +// CustomHTTPCookie.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// A struct that represents a custom HTTP cookie. +public struct CustomHTTPCookie: Codable { + var version: Int + var name: String? + var value: String? + var expiresDate: Date? + var isSessionOnly: Bool + var domain: String? + var path: String? + var isSecure: Bool + var isHTTPOnly: Bool + var comment: String? + var commentURL: URL? + var portList: [Int]? + var sameSitePolicy: String? + + enum CodingKeys: String, CodingKey { + case version + case name + case value + case expiresDate + case isSessionOnly + case domain + case path + case isSecure + case isHTTPOnly + case comment + case commentURL + case portList + case sameSitePolicy + } + + /// Initializes a `CustomHTTPCookie` from an `HTTPCookie`. + /// - Parameter cookie: The `HTTPCookie` to initialize from. + public init(from cookie: HTTPCookie) { + self.version = cookie.version + self.name = cookie.name + self.value = cookie.value + self.expiresDate = cookie.expiresDate + self.isSessionOnly = cookie.isSessionOnly + self.domain = cookie.domain + self.path = cookie.path + self.isSecure = cookie.isSecure + self.isHTTPOnly = cookie.isHTTPOnly + self.comment = cookie.comment + self.commentURL = cookie.commentURL + self.portList = cookie.portList?.map { $0.intValue } + self.sameSitePolicy = cookie.sameSitePolicy?.rawValue + } + + /// Converts the `CustomHTTPCookie` to an `HTTPCookie`. + /// - Returns: An `HTTPCookie` instance. + public func toHTTPCookie() -> HTTPCookie? { + var properties = [HTTPCookiePropertyKey: Any]() + properties[.version] = self.version + properties[.name] = self.name + properties[.value] = self.value + properties[.expires] = self.expiresDate + properties[.discard] = self.isSessionOnly ? Constants.true : nil + properties[.domain] = self.domain + properties[.path] = self.path + properties[.secure] = self.isSecure ? Constants.true : nil + properties[HTTPCookiePropertyKey(Constants.httpOnly)] = self.isHTTPOnly ? Constants.true : nil + properties[.comment] = self.comment + properties[.commentURL] = self.commentURL + properties[.port] = self.portList?.map { NSNumber(value: $0) } + + if let sameSitePolicyValue = self.sameSitePolicy { + properties[HTTPCookiePropertyKey.sameSitePolicy] = HTTPCookieStringPolicy(rawValue: sameSitePolicyValue) + } + + return HTTPCookie(properties: properties) + } + + enum Constants { + static let `true` = "TRUE" + static let httpOnly = "HttpOnly" + } +} diff --git a/Orchestrate/Orchestrate/CustomHeader.swift b/Orchestrate/Orchestrate/CustomHeader.swift new file mode 100644 index 0000000..1db32a6 --- /dev/null +++ b/Orchestrate/Orchestrate/CustomHeader.swift @@ -0,0 +1,51 @@ +// +// CustomHeader.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Configuration class for CustomHeader. +/// Allows adding custom headers to be injected into requests. +public class CustomHeaderConfig { + internal var headers = [(String, String)]() + + /// Adds a custom header to the configuration. + /// - Parameters: + /// - name: The name of the header. + /// - value: The value of the header. + public func header(name: String, value: String) { + headers.append((name, value)) + } +} + + +/// Module for injecting custom headers into requests. +public class CustomHeader { + + /// Initializes a new instance of `CustomHeader`. + public init() {} + + /// The module configuration. + public static let config: Module = Module.of({ CustomHeaderConfig() }) { setup in + setup.start { flowContext, request in + setup.config.headers.forEach { name, value in + request.header(name: name, value: value) + } + return request + } + + setup.next { flowContext, _, request in + setup.config.headers.forEach { name, value in + request.header(name: name, value: value) + } + return request + } + } +} diff --git a/Orchestrate/Orchestrate/HttpClient.swift b/Orchestrate/Orchestrate/HttpClient.swift new file mode 100644 index 0000000..c012151 --- /dev/null +++ b/Orchestrate/Orchestrate/HttpClient.swift @@ -0,0 +1,102 @@ +// +// HttpClient.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingLogger + +/// `HttpClient` is responsible for handling HTTP requests and logging the details of those requests and responses. +public class HttpClient { + var session: URLSession + /// The timeout interval for HTTP requests. + public var timeoutIntervalForRequest: TimeInterval = 60.0 + + /// Initializes a new instance of `HttpClient`. + /// - Parameter session: The URLSession instance to be used for HTTP requests. Defaults to a session with `RedirectPreventer` delegate. + public init(session: URLSession = URLSession(configuration: URLSessionConfiguration.default, + delegate: RedirectPreventer(), delegateQueue: nil)) { + self.session = session + } + + /// Logs the details of an HTTP request. + /// - Parameter request: The URLRequest to be logged. + public func logRequest(request: URLRequest?) { + if let request = request { + var log = "⬆\n" + log += "Request URL: \(request.url?.absoluteString ?? "")\n" + log += "Request Method: \(request.httpMethod ?? "")\n" + if let headers = request.allHTTPHeaderFields { + log += "Request Headers: \(headers)\n" + } + if let bodyData = request.httpBody, let bodyString = String(data: bodyData, encoding: .utf8) { + log += "Request Body: \(bodyString)\n" + } + log += "Request Timeout: \(request.timeoutInterval)\n" + LogManager.standard.d(log) + } + } + + /// Logs the details of an HTTP response. + /// - Parameter responseData: The data returned by the server. + /// - Parameter response: The URLResponse object containing the response metadata. + func logResponse(responseData: Data?, response: URLResponse?) { + var log = "⬇\n" + if let httpResponse = response as? HTTPURLResponse { + log += "Response Status Code: \(httpResponse.statusCode)\n" + log += "Response Headers: \(httpResponse.allHeaderFields)\n" + log += "Response Value: \(httpResponse.debugDescription)\n" + } + + if let data = responseData, let dataString = String(data: data, encoding: .utf8) { + log += "Response Data: \(dataString)" + } + LogManager.standard.d(log) + } + + /// Sends an HTTP request and returns the response data and metadata. + /// - Parameter request: The URLRequest to be sent. + /// - Throws: An error if the request fails. + /// - Returns: A tuple containing the response data and metadata. + func sendRequest(request: URLRequest) async throws -> (Data, URLResponse) { + var request = request + request.timeoutInterval = timeoutIntervalForRequest + logRequest(request: request) + let (data, response) = try await session.data(for: request) + logResponse(responseData: data, response: response) + return (data, response) + } + + /// Sends an HTTP request and returns the response data and metadata. + /// - Parameter request: The Request object to be sent. + /// - Throws: An error if the request fails. + /// - Returns: A tuple containing the response data and metadata. + public func sendRequest(request: Request) async throws -> (Data, URLResponse) { + return try await sendRequest(request: request.urlRequest) + } +} + + +/// RedirectPreventer` is a delegate class that prevents HTTP redirects during URL sessions. +/// This class conforms to `URLSessionDelegate` and `URLSessionTaskDelegate` to handle the redirection logic. +/// It ensures that any HTTP redirection responses are not followed by the `URLSession`. +public final class RedirectPreventer: NSObject, URLSessionDelegate, URLSessionTaskDelegate { + /// Called when the session receives a redirection response. + /// This method prevents the redirection by passing `nil` to the `completionHandler`. + /// - Parameters: + /// - session: The session containing the task that received a redirect. + /// - task: The task whose request resulted in a redirect. + /// - response: The response that caused the redirect. + /// - request: A URL request object filled out with the new location. + /// - completionHandler: A closure to call with the URL request to allow the redirection, or `nil` to prevent it. + public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { + // Prevent the redirect by passing nil to the completionHandler + completionHandler(nil) + } +} diff --git a/Orchestrate/Orchestrate/Module.swift b/Orchestrate/Orchestrate/Module.swift new file mode 100644 index 0000000..4d02173 --- /dev/null +++ b/Orchestrate/Orchestrate/Module.swift @@ -0,0 +1,51 @@ +// +// Module.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// A Module represents a unit of functionality in the application. +/// - property config: A function that returns the configuration for the module. +/// - property setup: A function that sets up the module. +public class Module: Equatable { + public private(set) var setup: (Setup) -> (Void) + public private(set) var config: () -> (ModuleConfig) + /// The unique identifier of the module. + public var id: UUID = UUID() + + /// Constructs a module with config. + /// - Parameters: + /// - config: A function that returns the configuration for the module. + /// - setup: A function that sets up the module. + public init(config: @escaping () -> (ModuleConfig), setup: @escaping (Setup) -> (Void)) { + self.setup = setup + self.config = config + } + + /// Constructs a module with config. + /// - Parameters: + /// - config: A function that returns the configuration for the module. + /// - setup: A function that sets up the module. + /// - Returns: A Module with the provided config. + public static func of(_ config: @escaping (() -> (ModuleConfig)) = {}, + setup: @escaping (Setup) -> (Void) + ) -> Module { + return Module(config: config, setup: setup) + } + + /// Compares two modules. + /// - Parameters: + /// - lhs: The left-hand module. + /// - rhs: The right-hand module. + /// - Returns: A boolean value indicating whether the two modules are equal. + public static func == (lhs: Module, rhs: Module) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/Orchestrate/Orchestrate/ModuleRegistry.swift b/Orchestrate/Orchestrate/ModuleRegistry.swift new file mode 100644 index 0000000..41a879e --- /dev/null +++ b/Orchestrate/Orchestrate/ModuleRegistry.swift @@ -0,0 +1,79 @@ +// +// ModuleRegistry.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Represents a ModuleRegistry protocol. A ModuleRegistry represents a registry of modules in the application. +public protocol ModuleRegistryProtocol { + associatedtype Config: Any + /// The UUID of the module + var id: UUID { get set } + /// The priority of the module in the registry. + var priority: Int { get } + /// The configuration for the module. + var config: Config { get } + /// The function that sets up the module. + var setup: (Setup) -> (Void) { get } + + /// Registers the module to the workflow. + func register(workflow: Workflow) +} + + +/// Class for a ModuleRegistry. A ModuleRegistry represents a registry of modules in the application. +/// - property id: The UUID of the module +/// - property priority: The priority of the module in the registry. +/// - property config: The configuration for the module. +/// - property setup: The function that sets up the module. +public class ModuleRegistry: ModuleRegistryProtocol { + public var id: UUID = UUID() + public let priority: Int + public let config: Config + public let setup: (Setup) -> Void + + public init(setup: @escaping (Setup) -> (Void), + priority: Int, + id: UUID, + config: Config) { + self.id = id + self.priority = priority + self.config = config + self.setup = setup + } + + /// Registers the module to the workflow. + /// - parameter workflow: The workflow to which the module is registered. + public func register(workflow: Workflow) { + let setupInstance = Setup(workflow: workflow, config: config) + setup(setupInstance) + } +} + + +extension ModuleRegistry: Comparable { + /// Compares two ModuleRegistry instances. + /// - Parameters: + /// - lhs: The left-hand side ModuleRegistry instance. + /// - rhs: The right-hand side ModuleRegistry instance. + /// - Returns: A boolean value indicating whether the left-hand side ModuleRegistry instance is less than the right-hand side ModuleRegistry instance. + public static func < (lhs: ModuleRegistry, rhs: ModuleRegistry) -> Bool { + return lhs.priority < rhs.priority + } + + /// Compares two ModuleRegistry instances for equality. + /// - Parameters: + /// - lhs: The left-hand side ModuleRegistry instance + /// - rhs: The right-hand side ModuleRegistry instance + /// - Returns: A boolean value indicating whether the left-hand side ModuleRegistry instance is equal to the right-hand side ModuleRegistry instance. + public static func == (lhs: ModuleRegistry, rhs: ModuleRegistry) -> Bool { + return lhs.priority == rhs.priority + } +} diff --git a/Orchestrate/Orchestrate/Node.swift b/Orchestrate/Orchestrate/Node.swift new file mode 100644 index 0000000..0211f15 --- /dev/null +++ b/Orchestrate/Orchestrate/Node.swift @@ -0,0 +1,146 @@ +// +// Node.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +/// Protocol for actions +public protocol Action {} + + +/// Protocol for closeable resources +public protocol Closeable { + func close() +} + + +/// Protocol for Node. Represents a node in the workflow. +public protocol Node {} + + +/// Represents an EmptyNode node in the workflow. +public struct EmptyNode: Node { + /// Initializes a new instance of `EmptyNode`. + public init() {} +} + + +/// Represents an Failure node in the workflow. +/// - property cause: The cause of the error. +public struct FailureNode: Node { + /// The cause of the error. + public let cause: Error + + /// Initializes a new instance of `FailureNode`. + /// - Parameter cause: The cause of the error. + public init(cause: any Error) { + self.cause = cause + } +} + + +/// Represents a ErrorNode node in the workflow. +/// - property status: The status of the error. +/// - property input: The input for the error. +/// - property message: The message for the error. +public struct ErrorNode: Node { + public let input: [String: Any] + public let message: String + public let status: Int? + + /// Initializes a new instance of `ErrorNode`. + /// - Parameters: + /// - status: The status of the error. + /// - input: The input for the error. + /// - message: The message for the error. + public init(status: Int? = nil, + input: [String : Any] = [:], + message: String = "") { + self.input = input + self.message = message + self.status = status + } +} + + +/// Represents a success node in the workflow. +/// - property input: The input for the success. +/// - property session: The session for the success. +public struct SuccessNode: Node { + public let input: [String: Any] + public let session: Session + + /// Initializes a new instance of `SuccessNode`. + /// - Parameters: + /// - input: The input for the success. + /// - session: The session for the success. + public init(input: [String : Any] = [:], session: Session) { + self.session = session + self.input = input + } +} + + +/// Abstract class for a ContinueNode node in the workflow. +/// - property context: The context for the node. +/// - property workflow: The workflow for the node. +/// - property input: The input for the node. +/// - property actions: The actions for the node. +open class ContinueNode: Node, Closeable { + public let context: FlowContext + public let workflow: Workflow + public let input: [String: Any] + public let actions: [any Action] + + /// Initializes a new instance of `ContinueNode`. + /// - Parameters: + /// - context: The context for the node. + /// - workflow: The workflow for the node. + /// - input: The input for the node. + /// - actions: The actions for the node. + public init(context: FlowContext, workflow: Workflow, input: [String: Any], actions: [any Action]) { + self.context = context + self.workflow = workflow + self.input = input + self.actions = actions + } + + /// Converts the ContinueNode to a Request. + /// - Returns: The Request representation of the ContinueNode. + open func asRequest() -> Request { + fatalError("Must be overridden in subclass") + } + + /// Moves to the next node in the workflow. + /// - Returns: The next Node. + public func next() async -> Node { + return await workflow.next(context, self) + } + + /// Closes all closeable actions. + public func close() { + actions.compactMap { $0 as? Closeable }.forEach { $0.close() } + } +} + + +/// Protocol for a Session. A Session represents a user's session in the application. +public protocol Session { + /// Returns the value of the session as a String. + var value: String { get } +} + + +/// Singleton for an EmptySession. An EmptySession represents a session with no value. +public struct EmptySession: Session { + /// The value of the empty session as a String. + public var value: String = "" + + /// Initializes a new instance of `EmptySession`. + public init() {} +} diff --git a/Orchestrate/Orchestrate/Orchestrate.h b/Orchestrate/Orchestrate/Orchestrate.h new file mode 100644 index 0000000..193c1e7 --- /dev/null +++ b/Orchestrate/Orchestrate/Orchestrate.h @@ -0,0 +1,21 @@ +// +// Orchestrate.h +// Orchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +#import + +//! Project version number for Orchestrate. +FOUNDATION_EXPORT double OrchestrateVersionNumber; + +//! Project version string for Orchestrate. +FOUNDATION_EXPORT const unsigned char OrchestrateVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Orchestrate/Orchestrate/PrivacyInfo.xcprivacy b/Orchestrate/Orchestrate/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..fcfc9b9 --- /dev/null +++ b/Orchestrate/Orchestrate/PrivacyInfo.xcprivacy @@ -0,0 +1,10 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/Orchestrate/Orchestrate/Request.swift b/Orchestrate/Orchestrate/Request.swift new file mode 100644 index 0000000..69d062d --- /dev/null +++ b/Orchestrate/Orchestrate/Request.swift @@ -0,0 +1,122 @@ +// +// SampleRequest.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import UIKit + +/// Class for a Request. A Request represents a request to be sent over the network. +public class Request { + + /// The URL request. + public private(set) var urlRequest: URLRequest = URLRequest(url: URL(string: "https://")!) + + /// Initializes a Request with a URL. + /// - Parameter urlString: The URL of the request. + public init(urlString: String = "https://") { + self.urlRequest.url = URL(string: urlString)! + } + + /// Sets the URL of the request. + /// - Parameter urlString: The URL to be set. + public func url(_ urlString: String) { + if let url = URL(string: urlString) { + self.urlRequest.url = url + // keeping Default Content type + self.header(name: Constants.contentType, value: ContentType.json.rawValue) + } + } + + /// Adds a parameter to the request. + /// - Parameters: + /// - name: The name of the parameter. + /// - value: The value of the parameter. + public func parameter(name: String, value: String) { + if let url = self.urlRequest.url { + if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + if components.queryItems == nil { + components.queryItems = [] + } + components.queryItems?.append(URLQueryItem(name: name, value: value)) + if let updatedURL = components.url { + self.urlRequest.url = updatedURL + } + } + } + } + + /// Adds a header to the request. + /// - Parameters: + /// - name: The name of the header. + /// - value: The value of the header. + public func header(name: String, value: String) { + self.urlRequest.setValue(value, forHTTPHeaderField: name) + } + + /// Adds cookies to the request. + /// - Parameter cookies: The cookies to be added. + public func cookies(cookies: [HTTPCookie]) { + let headers = HTTPCookie.requestHeaderFields(with: cookies) + for (key, value) in headers { + self.urlRequest.addValue(value, forHTTPHeaderField: key) + } + } + + /// Sets the body of the request. + /// - Parameter body: The body to be set. + public func body(body: [String: Any]) { + self.urlRequest.httpMethod = HTTPMethod.post.rawValue + self.urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: Constants.contentType) + self.urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) + } + + /// Sets the form of the request. + /// - Parameter formData: The form to be set. + public func form(formData: [String: String]) { + var formString = "" + for (key, value) in formData { + formString += "\(key)=\(value)&" + } + if !formString.isEmpty { + formString.removeLast() // Remove the last '&' character + } + + self.urlRequest.httpMethod = HTTPMethod.post.rawValue + self.urlRequest.setValue(ContentType.urlEncoded.rawValue, forHTTPHeaderField: Constants.contentType) + self.urlRequest.httpBody = formString.data(using: .utf8) + } + + /// Represents various content types used in HTTP requests. + public enum ContentType: String { + case plainText = "text/plain" + case json = "application/json" + case urlEncoded = "application/x-www-form-urlencoded" + } + + /// Represents HTTP methods used in network requests. + public enum HTTPMethod: String { + case get = "GET" + case put = "PUT" + case post = "POST" + case delete = "DELETE" + } + + /// Represents various constants used in network requests. + public enum Constants { + public static let contentType = "Content-Type" + public static let accept = "Accept" + public static let xRequestedWith = "x-requested-with" + public static let xRequestedPlatform = "x-requested-platform" + public static let pingSdk = "ping-sdk" + public static let ios = "ios" + public static let stCookie = "ST" + public static let stNoSsCookie = "ST-NO-SS" + } +} diff --git a/Orchestrate/Orchestrate/Response.swift b/Orchestrate/Orchestrate/Response.swift new file mode 100644 index 0000000..31ab14a --- /dev/null +++ b/Orchestrate/Orchestrate/Response.swift @@ -0,0 +1,58 @@ +// +// SampleRequest.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Struct for a Response. A Response represents a response received from a network request. +/// - property data: The data received from the network request. +/// - property response: The URLResponse received from the network request. +public struct Response { + public let data: Data + public let response: URLResponse + + /// Returns the body of the response. + /// - Returns: The body of the response as a String. + public func body() -> String { + return String(data: data, encoding: .utf8) ?? "" + } + + /// Returns the body of the response as a JSON object. + /// - Parameter data: The data to convert to a JSON object. + /// - Returns: The body of the response as a JSON object. + public func json(data: Data) throws -> [String: Any] { + return (try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:] + } + + /// Returns the status code of the response. + /// - Returns: The status code of the response as an Int. + public func status() -> Int { + return (response as? HTTPURLResponse)?.statusCode ?? 0 + } + + /// Returns the value of a specific header from the response. + /// - Parameter name: The name of the header. + /// - Returns: The value of the header as a String. + public func header(name: String) -> String? { + return (response as? HTTPURLResponse)?.allHeaderFields[name] as? String + } + + /// Returns the cookies from the response. + /// - Returns: The cookies from the response as an array of HTTPCookie. + public func getCookies() -> [HTTPCookie] { + if let response = (response as? HTTPURLResponse), + let allHeaders = response.allHeaderFields as? [String : String], + let url = response.url { + let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaders, for: url) + return cookies + } + return [] + } +} diff --git a/Orchestrate/Orchestrate/Setup.swift b/Orchestrate/Orchestrate/Setup.swift new file mode 100644 index 0000000..e0d7f8d --- /dev/null +++ b/Orchestrate/Orchestrate/Setup.swift @@ -0,0 +1,87 @@ +// +// Setup.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingLogger + +/// Struct for a Setup. A Setup represents the setup of a module in the application. +/// - property workflow: The workflow of the application. +/// - property context: The shared context of the application. +/// - property logger: The logger used in the application. +/// - property httpClient: The HTTP client used in the application. +/// - property config: The configuration for the module. +public struct Setup { + public let workflow: Workflow + public let context: SharedContext + public let logger: Logger + public let httpClient: HttpClient + public let config: ModuleConfig + + /// Initializes a new Setup instance. + /// - Parameters: + /// - workflow: The workflow of the application. + /// - config: The configuration for the module. + public init(workflow: Workflow, config: ModuleConfig) { + self.workflow = workflow + self.context = workflow.sharedContext + self.logger = workflow.config.logger + self.httpClient = workflow.config.httpClient + self.config = config + } + + /// Adds an initialization block to the workflow. + /// - Parameter block: The block to be added. + public func initialize(block: @escaping() async throws -> Void) { + workflow.initHandlers.append(block) + } + + /// Adds a start block to the workflow. + /// - Parameter block: The block to be added. + public func start(_ block: @escaping (FlowContext, Request) async throws -> Request) { + workflow.startHandlers.append(block) + } + + /// Adds a next block to the workflow. + /// - Parameter block: The block to be added. + public func next(block: @escaping (FlowContext, ContinueNode, Request) async throws -> Request) { + workflow.nextHandlers.append(block) + } + + /// Adds a response block to the workflow. + /// - Parameter block: The block to be added. + public func response(block: @escaping (FlowContext, Response) async throws -> Void) { + workflow.responseHandlers.append(block) + } + + /// Adds a node block to the workflow. + /// - Parameter block: The block to be added. + public func node(block: @escaping (FlowContext, Node) async throws -> Node) { + workflow.nodeHandlers.append(block) + } + + /// Adds a success block to the workflow. + /// - Parameter block: The block to be added. + public func success(block: @escaping (FlowContext, SuccessNode) async throws -> SuccessNode) { + workflow.successHandlers.append(block) + } + + /// Sets the transform block of the workflow. + /// - Parameter block: The block to be set. + public func transform(block: @escaping (FlowContext, Response) async throws -> Node) { + workflow.transformHandler = block + } + + /// Adds a sign off block to the workflow. + /// - Parameter block: The block to be added. + public func signOff(block: @escaping (Request) async -> Request) { + workflow.signOffHandlers.append(block) + } +} diff --git a/Orchestrate/Orchestrate/SharedContext.swift b/Orchestrate/Orchestrate/SharedContext.swift new file mode 100644 index 0000000..de426ae --- /dev/null +++ b/Orchestrate/Orchestrate/SharedContext.swift @@ -0,0 +1,68 @@ +// +// SharedContext.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// A class that manages a shared context using a dictionary. +public class SharedContext { + private var map: [String: Any] = [:] + private var queue = DispatchQueue(label: "shared.conext.queue", attributes: .concurrent) + + /// Initializes the SharedContext with an empty dictionary or a pre-existing one. + /// + /// - Parameter map: A dictionary to initialize the context with. Defaults to an empty dictionary. + public init(_ map: [String: Any] = [:]) { + queue.sync(flags: .barrier) { + self.map = map + } + } + + /// Sets a value for the given key in the shared context. + /// + /// - Parameters: + /// - key: The key for which to set the value. + /// - value: The value to set for the given key. + public func set(key: String, value: Any) { + queue.sync(flags: .barrier) { + self.map[key] = value + } + } + + /// Retrieves the value for the given key from the shared context. + /// + /// - Parameter key: The key for which to get the value. + /// - Returns: The value associated with the key, or `nil` if the key does not exist. + public func get(key: String) -> Any? { + queue.sync { + return self.map[key] + } + } + + /// Removes the value for the given key from the shared context. + /// + /// - Parameter key: The key for which to remove the value. + /// - Returns: The removed value, or `nil` if the key does not exist. + public func removeValue(forKey key: String) -> Any? { + queue.sync(flags: .barrier) { + self.map.removeValue(forKey: key) + } + } + + /// A Boolean value indicating whether the shared context is empty. + public var isEmpty: Bool { + queue.sync { + return self.map.isEmpty + } + } + + /// A namespace for key names to be added in an extension. + public enum Keys { } +} diff --git a/Orchestrate/Orchestrate/WorkFlow.swift b/Orchestrate/Orchestrate/WorkFlow.swift new file mode 100644 index 0000000..0ba7e60 --- /dev/null +++ b/Orchestrate/Orchestrate/WorkFlow.swift @@ -0,0 +1,221 @@ +// +// Workflow.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingLogger + +/// Class representing the context of a flow. +/// - property flowContext: The shared context of the flow. +public class FlowContext { + public let flowContext: SharedContext + + public init(flowContext: SharedContext) { + self.flowContext = flowContext + } +} + + +extension Workflow { + /// Creates a new Workflow instance with the provided configuration block. + /// - Parameter block: The configuration block for the Workflow. + /// - Returns: A new Workflow instance. + public static func createWorkflow(_ block: (WorkflowConfig) -> Void = { _ in }) -> Workflow { + let config = WorkflowConfig() + block(config) + return Workflow(config: config) + } +} + + +/// Enum representing the keys for the modules. +public enum ModuleKeys: String { + case customHeader = "customHeader" + case nosession = "nosession" + case forceAuth = "forceAuth" +} + + +/// Class representing a workflow. +public class Workflow { + /// The configuration for the workflow. + public let config: WorkflowConfig + /// Global SharedContext + public let sharedContext = SharedContext() + + private var started = false + + internal var initHandlers = [() async throws -> Void]() + internal var startHandlers = [(FlowContext, Request) async throws -> Request]() + internal var nextHandlers = [(FlowContext, ContinueNode, Request) async throws -> Request]() + internal var responseHandlers = [(FlowContext, Response) async throws -> Void]() + internal var nodeHandlers = [(FlowContext, Node) async throws -> Node]() + internal var successHandlers = [(FlowContext, SuccessNode) async throws -> SuccessNode]() + internal var signOffHandlers = [(Request) async throws -> Request]() + // Transform response to Node, we can only have one transform + internal var transformHandler: (FlowContext, Response) async throws -> Node = { _, _ in EmptyNode() } + + /// Initializes the workflow. + /// - Parameter config: The configuration for the workflow. + public init(config: WorkflowConfig) { + self.config = config + self.config.register(workflow: self) + } + + /// Initializes the workflow. + public func initialize() async throws { + if !started { + var tasks: [Task] = [] + // Create tasks for each handler + for handler in initHandlers { + let task = Task { + try await handler() + } + tasks.append(task) + } + + // Await all tasks to complete + for task in tasks { + try await task.value + } + started = true + } + } + + /// Starts the workflow with the provided request. + /// - Parameter request: The request to start the workflow with. + /// - Returns: The resulting `Node` after processing the workflow. + private func start(request: Request) async throws -> Node { + // Before we start, make sure all the module init has been completed + try await initialize() + config.logger.i("Starting...") + let context = FlowContext(flowContext: SharedContext()) + var currentRequest = request + for handler in startHandlers { + currentRequest = try await handler(context, currentRequest) + } + let response = try await send(context, request: currentRequest) + + let transform = try await transformHandler(context, response) + + var initialNode = transform + for handler in nodeHandlers { + initialNode = try await handler(context, initialNode) + } + + return try await next(context, initialNode) + } + + /// Starts the workflow with a default request. + /// - Returns: The resulting `Node` after processing the workflow. + public func start() async -> Node { + do { + return try await start(request: Request()) + } + catch { + return FailureNode(cause: error) + } + } + + /// Sends a request and returns the response. + /// - Parameters: + /// - context: The context of the flow. + /// - request: The request to be sent. + /// - Returns: The response received. + private func send(_ context: FlowContext, request: Request) async throws -> Response { + let (data, urlResponse) = try await config.httpClient.sendRequest(request: request) + let response = Response(data: data, response: urlResponse) + for handler in responseHandlers { + try await handler(context, response) + } + return response + } + + /// Sends a request and returns the response. + /// - Parameter request: The request to be sent. + /// - Returns: The response received. + private func send(_ request: Request) async throws -> Response { + // semaphore + let (data, urlResponse) = try await config.httpClient.sendRequest(request: request) + return Response(data: data, response: urlResponse) + } + + /// Processes the next node if it is a success node. + /// - Parameters: + /// - context: The context of the flow. + /// - node: The current node. + /// - Returns: The resulting `Node` after processing the next step. + private func next(_ context: FlowContext, _ node: Node) async throws -> Node { + if let success = node as? SuccessNode { + var result = success + for handler in successHandlers { + result = try await handler(context, result) + } + return result + } else { + return node + } + } + + /// Processes the next node in the workflow. + /// - Parameters: + /// - context: The context of the flow. + /// - current: The current ContinueNode. + /// - Returns: The resulting Node after processing the next step. + public func next(_ context: FlowContext, _ current: ContinueNode) async -> Node { + do { + config.logger.i("Next...") + let initialRequest = current.asRequest() + var request = initialRequest + for handler in nextHandlers { + request = try await handler(context, current, request) + } + current.close() + let initialNode = try await transformHandler(context, try await send(context, request: request)) + var node = initialNode + for handler in nodeHandlers { + node = try await handler(context, node) + } + return try await next(context, node) + } + catch { + return FailureNode(cause: error) + } + } + + /// Signs off the workflow. + /// - Returns: A Result indicating the success or failure of the sign off. + public func signOff() async -> Result { + self.config.logger.i("SignOff...") + do { + try await initialize() + var request = Request() + for handler in signOffHandlers { + request = try await handler(request) + } + _ = try await send(request) + return .success(()) + } + catch { + config.logger.e("Error during sign off", error: error) + return .failure(error) + } + } + + /// Processes the response. + /// - Parameters: + /// - context: The context of the flow. + /// - response: The response to be processed. + private func response(context: FlowContext, response: Response) async throws { + for handler in responseHandlers { + try await handler(context, response) + } + } +} diff --git a/Orchestrate/Orchestrate/WorkFlowConfig.swift b/Orchestrate/Orchestrate/WorkFlowConfig.swift new file mode 100644 index 0000000..99fb4e2 --- /dev/null +++ b/Orchestrate/Orchestrate/WorkFlowConfig.swift @@ -0,0 +1,91 @@ +// +// WorkflowConfig.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingLogger + +/// Enum representing the mode of module override. +public enum OverrideMode { + case override // Override the previous registered module + case append // Append to the list, and cannot be overridden + case ignore // Ignore if the module is already registered +} + + +/// Workflow configuration +public class WorkflowConfig { + /// Use a list instead of a map to allow registering a module twice with different configurations + public private(set) var modules: [any ModuleRegistryProtocol] = [] + /// Timeout for the HTTP client, default is 15 seconds + public var timeout: TimeInterval = 15.0 + /// Logger for the log, default is NoneLogger + public var logger: Logger = LogManager.logger { + didSet { + // Propagate the logger to Modules + LogManager.logger = logger + } + } + /// HTTP client for the engine + public internal(set) var httpClient: HttpClient = HttpClient() + + /// Initializes a new WorkflowConfig instance. + public init() {} + + /// Register a module. + /// - Parameters: + /// - module: The module to be registered. + /// - priority: The priority of the module in the registry. Default is 10. + /// - mode: The mode of the module registration. Default is `override`. If the mode is `override`, the module will be overridden if it is already registered. + /// - config: The configuration for the module. + public func module(_ module: Module, + _ priority: Int = 10, + mode: OverrideMode = .override, + _ config: @escaping (T) -> (Void) = { _ in }) { + + switch mode { + case .override: + + if let index = modules.firstIndex(where: { $0.id == module.id }) { + modules[index] = ModuleRegistry(setup: module.setup, priority: modules[index].priority, id: modules[index].id, config: configValue(initalValue: module.config, nextValue: config)) + } else { + let registry = ModuleRegistry(setup: module.setup, priority: priority, id: module.id, config: configValue(initalValue: module.config, nextValue: config)) + modules.append(registry ) + } + + case .append: + let uuid = UUID() + let moduleCopy = module + let registry = ModuleRegistry(setup: moduleCopy.setup, priority: priority, id: uuid, config: configValue(initalValue: moduleCopy.config, nextValue: config)) + modules.append(registry) + + case .ignore: + if modules.contains(where: { $0.id == module.id }) { + return + } + let registry = ModuleRegistry(setup: module.setup, priority: priority, id: module.id, config: configValue(initalValue: module.config, nextValue: config)) + modules.append(registry) + } + } + + private func configValue(initalValue: @escaping () -> (T), nextValue: @escaping (T) -> (Void)) -> T { + let initConfig = initalValue() + nextValue(initConfig) + return initConfig + } + + /// Registers the workflow + /// - Parameter workflow: The workflow to be registered. + public func register(workflow: Workflow) { + httpClient.timeoutIntervalForRequest = timeout + modules.sort(by: { $0.priority < $1.priority }) + modules.forEach { $0.register(workflow: workflow) } + } +} diff --git a/Orchestrate/OrchestrateTests/CookieModuleTests.swift b/Orchestrate/OrchestrateTests/CookieModuleTests.swift new file mode 100644 index 0000000..f0709df --- /dev/null +++ b/Orchestrate/OrchestrateTests/CookieModuleTests.swift @@ -0,0 +1,202 @@ +// +// CookieModuleTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingStorage +@testable import PingOrchestrate + +final class CookieModuleTests: XCTestCase { + + override func setUp() { + super.setUp() + MockURLProtocol.startInterceptingRequests() + } + + override func tearDown() { + super.tearDown() + MockURLProtocol.stopInterceptingRequests() + } + + func testCookieFromResponse() async { + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: URL(string: "http://openam.example.com")!, statusCode: 200, httpVersion: nil, headerFields: [ + "Set-Cookie": + "interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com, interactionToken=abc; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com" + ])!, Data()) + } + + let dummy = Module.of({CustomHeaderConfig()}) { module in + module.transform {_,_ in + return SuccessNode(session: EmptySession()) + } + } + let memory = MemoryStorage<[CustomHTTPCookie]>() + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(dummy) + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = memory + cookieValue.persist = ["interactionId", "interactionToken"] + } + } + + _ = await workflow.start() + let cookies = try? await memory.get() + XCTAssertNotNil(cookies) + XCTAssertEqual(cookies?.count, 2) + } + + func testCookieStorageFromResponse() async { + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: URL(string: "http://openam.example.com")!, statusCode: 200, httpVersion: nil, headerFields: [ + "Set-Cookie": + "interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=.openam.example.com, interactionToken=abc; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=.openam.example.com" + ])!, Data()) + } + + let dummy = Module.of({CustomHeaderConfig()}) { module in + module.transform {_,_ in + return SuccessNode(session: EmptySession()) + } + } + let memory = MemoryStorage<[CustomHTTPCookie]>() + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(dummy) + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = memory + cookieValue.persist = ["interactionId"] + } + } + + _ = await workflow.start() + let cookies = try? await memory.get() + XCTAssertNotNil(cookies) + XCTAssertEqual(cookies?.count, 1) + } + + func testCookieInjectToRequestAndSignoff() async { + var success = false + let json: [String: Any] = ["booleanKey": true] + + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: URL(string: "http://openam.example.com")!, statusCode: 200, httpVersion: nil, headerFields: [ + "Set-Cookie": + "interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com, interactionToken=abc; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com" + ])!, Data()) + } + + var workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + } + + let dummy = Module.of({CustomHeaderConfig()}) { module in + module.transform { flowContext, _ in + if success { + return SuccessNode(session: EmptySession()) + } else { + success = true + return TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: []) + } + } + } + let memory = MemoryStorage<[CustomHTTPCookie]>() + workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(dummy) + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = memory + cookieValue.persist = ["interactionId"] + } + } + + let node = await workflow.start() + _ = await (node as? ContinueNode)?.next() + + XCTAssertTrue(MockURLProtocol.requestHistory[1].allHTTPHeaderFields!["Cookie"]!.contains("interactionId=178ce234-afd2-4207-984e-bda28bd7042c")) + XCTAssertTrue(MockURLProtocol.requestHistory[1].allHTTPHeaderFields!["Cookie"]!.contains("interactionToken=abc")) + + let cookies = try? await memory.get() + XCTAssertNotNil(cookies) + XCTAssertEqual(cookies?.count, 1) + + let result = await workflow.signOff() + switch result { + case .success: + break + default: + XCTFail("Should have succeeded") + } + +// XCTAssertTrue(MockURLProtocol.requestHistory[2].allHTTPHeaderFields!["Cookie"]!.contains("interactionId=178ce234-afd2-4207-984e-bda28bd7042c")) +// XCTAssertFalse(MockURLProtocol.requestHistory[2].allHTTPHeaderFields!["Cookie"]!.contains("interactionToken=abc")) + let cookies2 = try? await memory.get() + XCTAssertNil(cookies2) + + } + + func testExpiredCookieFromResponse() async { + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: URL(string: "http://openam.example.com")!, statusCode: 200, httpVersion: nil, headerFields: [ + "Set-Cookie": + "interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Path=/; Expires=Wed, 21 Oct 1999 01:00:00 GMT; HttpOnly; Domain=openam.example.com, interactionToken=abc; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com" + ])!, Data()) + } + + let dummy = Module.of({CustomHeaderConfig()}) { module in + module.transform {_,_ in + return SuccessNode(session: EmptySession()) + } + } + let memory = MemoryStorage<[CustomHTTPCookie]>() + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(dummy) + + config.module(CookieModule.config) { cookieValue in + cookieValue.cookieStorage = memory + cookieValue.persist = ["interactionId", "interactionToken"] + } + + } + + _ = await workflow.start() + let cookies = try? await memory.get() + XCTAssertNotNil(cookies) + XCTAssertEqual(cookies?.count, 1) + } + + + func testCookieIsExpiredValidationExpired() { + let setCookie: [String: String] = ["Set-Cookie":"iPlanetDirectoryPro=token; Expires=Wed, 21 Oct 1999 01:00:00 GMT; Domain=openam.example.com"] + let cookies = HTTPCookie.cookies(withResponseHeaderFields: setCookie, for: URL(string: "https://openam.example.com")!) + guard let cookie = cookies.first else { + XCTFail("Failed to parse Cookies from response header") + return + } + XCTAssertTrue(cookie.isExpired) + } + + + func testCookieIsExpiredValidationNotExpired() { + let setCookie: [String: String] = ["Set-Cookie":"iPlanetDirectoryPro=token; Expires=Wed, 21 Oct 2032 01:00:00 GMT; Domain=openam.example.com"] + let cookies = HTTPCookie.cookies(withResponseHeaderFields: setCookie, for: URL(string: "https://openam.example.com")!) + guard let cookie = cookies.first else { + XCTFail("Failed to parse Cookies from response header") + return + } + XCTAssertFalse(cookie.isExpired) + } +} diff --git a/Orchestrate/OrchestrateTests/CustomHeaderModuleTests.swift b/Orchestrate/OrchestrateTests/CustomHeaderModuleTests.swift new file mode 100644 index 0000000..d98a53e --- /dev/null +++ b/Orchestrate/OrchestrateTests/CustomHeaderModuleTests.swift @@ -0,0 +1,48 @@ +// +// CustomHeaderModuleTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingOrchestrate + +final class CustomHeaderModuleTest: XCTestCase { + + override func setUp() { + super.setUp() + MockURLProtocol.startInterceptingRequests() + } + + override func tearDown() { + super.tearDown() + MockURLProtocol.stopInterceptingRequests() + } + + func testCustomHeaderAddedToRequest() async { + MockURLProtocol.requestHandler = { request in + + XCTAssertNotNil(request.allHTTPHeaderFields?["X-Custom-Header"]) + XCTAssertEqual("CustomValue", request.allHTTPHeaderFields?["X-Custom-Header"]) + + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data()) + + } + + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(CustomHeader.config) { customHeaderConfig in + customHeaderConfig.header(name: "X-Custom-Header", value: "CustomValue") + + } + } + + _ = await workflow.start() + } +} diff --git a/Orchestrate/OrchestrateTests/FlowContextTests.swift b/Orchestrate/OrchestrateTests/FlowContextTests.swift new file mode 100644 index 0000000..6b332b0 --- /dev/null +++ b/Orchestrate/OrchestrateTests/FlowContextTests.swift @@ -0,0 +1,41 @@ +// +// FlowContextTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOrchestrate + +final class FlowContextTests: XCTestCase { + + func testInit() async throws { + let context = FlowContext(flowContext: SharedContext(["cookie": "pingCookie"])) + XCTAssertNotNil(context) + } + + func testDefaultValues() async throws { + let context = FlowContext(flowContext: SharedContext(["cookie": "pingCookie"])) + let request = Request() + let body = ["bodykey": "bodyvalue"] + request.url("https://pingone.com") + request.header(name: "testHeader", value: "testValue") + request.header(name: "testHeader1", value: "testValue2") + request.header(name: "testHeader1", value: "testValue2") + request.parameter(name: "key1", value: "key1Value") + request.parameter(name: "key2", value: "key2Value") + request.body(body: body) + + context.flowContext.set(key: "request", value: request) + let cookie = context.flowContext.get(key: "cookie") + XCTAssertNotNil(cookie) + let requestValue = context.flowContext.get(key: "request") + XCTAssertTrue(requestValue != nil) + XCTAssertTrue(requestValue is Request) + } +} diff --git a/Orchestrate/OrchestrateTests/ModuleTests.swift b/Orchestrate/OrchestrateTests/ModuleTests.swift new file mode 100644 index 0000000..def5870 --- /dev/null +++ b/Orchestrate/OrchestrateTests/ModuleTests.swift @@ -0,0 +1,46 @@ +// +// ModuleTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOrchestrate + +final class ModuleTests: XCTestCase { + + func testModule() async throws { + + class CustomHeaderConfig: Equatable { + var enable = true + var headerValue = "iOS-SDK" + var headerName = "header-name" + + static func == (lhs: CustomHeaderConfig, rhs: CustomHeaderConfig) -> Bool { + return lhs.enable == rhs.enable && lhs.headerValue == rhs.headerValue && lhs.headerName == rhs.headerName + } + } + + func headerconfig(setup: Setup) { + let config = setup.config + setup.next { (context, _, request) in + if config.enable { + request.header(name: config.headerName, value: config.headerValue) + } + return request + } + } + + let block = { CustomHeaderConfig() } + let module = Module.of(block, setup: headerconfig) + + XCTAssertNotNil(module.config) + XCTAssertNotNil(module.setup) + + } +} diff --git a/Orchestrate/OrchestrateTests/NodeTests.swift b/Orchestrate/OrchestrateTests/NodeTests.swift new file mode 100644 index 0000000..e8535ad --- /dev/null +++ b/Orchestrate/OrchestrateTests/NodeTests.swift @@ -0,0 +1,66 @@ +// +// NodeTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingOrchestrate + +final class NodeTests: XCTestCase { + + func testConnectorNextShouldReturnContinueNodeInWorkflow() async { + let mockWorkflow = WorkflowMock(config: WorkflowConfig()) + let mockContext = FlowContextMock(flowContext: SharedContext()) + let mockNode = NodeMock() + + mockWorkflow.nextReturnValue = mockNode + + let connector = TestContinueNode(context: mockContext, workflow: mockWorkflow, input: [:], actions: []) + + let continueNode = await connector.next() + XCTAssertTrue(continueNode as? NodeMock === mockNode) + } + + func testConnectorCloseShouldCloseAllCloseableActions() { + let closeableAction = TestAction() + let connector = TestContinueNode(context: FlowContextMock(flowContext: SharedContext()), workflow: WorkflowMock(config: WorkflowConfig()), input: [:], actions: [closeableAction]) + + connector.close() + + XCTAssertTrue(closeableAction.isClosed) + } +} + +// Supporting Test Classes +class WorkflowMock: Workflow { + var nextReturnValue: Node? + override func next(_ context: FlowContext, _ current: ContinueNode) async -> Node { + return nextReturnValue ?? NodeMock() + } +} + +class FlowContextMock: FlowContext {} + +class NodeMock: Node {} + +class TestContinueNode: ContinueNode { + override func asRequest() -> Request { + return RequestMock(urlString: "https://openam.example.com") + } +} + +class TestAction: Action, Closeable { + var isClosed = false + func close() { + isClosed = true + } +} + +class RequestMock: Request {} diff --git a/Orchestrate/OrchestrateTests/OrchestrateTests.swift b/Orchestrate/OrchestrateTests/OrchestrateTests.swift new file mode 100644 index 0000000..b54daf9 --- /dev/null +++ b/Orchestrate/OrchestrateTests/OrchestrateTests.swift @@ -0,0 +1,252 @@ +// +// OrchestrateTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +// +//import Testing +// +//@testable import PingOrchestrate +// +//struct OrchestrateTests { +// +// @Test func testWorkFlowOverridable() async throws { +// +// class CustomHeaderConfig { +// var enable = true +// var headerValue = "iOS-SDK" +// var headerName = "header-name" +// } +// +// let customHeader = Module.of({ CustomHeaderConfig() }, block: { setup in +// let config = setup.config +// setup.next { ( context, _, request) in +// if config.enable { +// request.header(name: config.headerName, value: config.headerValue) +// } +// return request +// } +// +// setup.start { ( context, request) in +// if config.enable { +// request.header(name: config.headerName, value: config.headerValue) +// } +// return request +// } +// +// }) +// +// +// let nosession = Module.of { setup in +// setup.next { ( context,_, request) in +// request.header(name: "nosession", value: "true") +// return request +// } +// } +// +// +// let forceAuth = Module.of { setup in +// setup.start { ( context, request) in +// request.header(name: "forceAuth", value: "true") +// return request +// } +// } +// +// let workFlow = Workflow.config { config in +// config.debug = true +// config.timeout = 10 +// +// config.module(customHeader) { header in +// header.headerName = "header-name2" +// header.headerValue = "iOS-SDK" +// } +// +// config.module(customHeader) { header in +// header.headerName = "header-name1" +// header.headerValue = "Android-SDK" +// } +// +// config.module(forceAuth) +// config.module(nosession) +// +// } +// +// #expect(workFlow.workFlowConfig.modules.count == 3) +// +// +// } +// +// @Test("This validates customer can be overridate later") func testWorkFlowOverridable1() async throws { +// +// class CustomHeaderConfig { +// var enable = true +// var headerValue = "iOS-SDK" +// var headerName = "header-name" +// } +// +// let customHeader = Module.of({ CustomHeaderConfig() }, block: { setup in +// let config = setup.config +// setup.next { ( context,_, request) in +// if config.enable { +// request.header(name: config.headerName, value: config.headerValue) +// } +// return request +// } +// +// setup.start { ( context, request) in +// if config.enable { +// request.header(name: config.headerName, value: config.headerValue) +// } +// return request +// } +// +// }) +// +// +// let nosession = Module.of { setup in +// setup.next { ( context, _, request) in +// request.header(name: "nosession", value: "true") +// return request +// } +// } +// +// +// let forceAuth = Module.of{ setup in +// setup.start { ( context, request) in +// request.header(name: "forceAuth", value: "true") +// return request +// } +// } +// +// let workFlow = Workflow.config { config in +// config.debug = true +// config.timeout = 10 +// +// config.module(customHeader, mode: OverrideMode.append) { header in +// header.enable = true +// header.headerName = "header-name1" +// header.headerValue = "Android-SDK1" +// } +// +// config.module(customHeader) { header in +// header.enable = true +// header.headerName = "header-name2" +// header.headerValue = "Android-SDK2" +// } +// +// config.module(customHeader, mode: OverrideMode.append) { header in +// header.enable = true +// header.headerName = "header-name3" +// header.headerValue = "iOS-SDK3" +// } +// +// +// config.module(customHeader) { header in +// header.enable = true +// header.headerName = "header-name4" +// header.headerValue = "Android-SDK4" +// } +// +// +// config.module(forceAuth) +// config.module(nosession) +// +// } +// +// try #require(workFlow.workFlowConfig.modules.count > 0) +// +// #expect(workFlow.workFlowConfig.modules.count == 5) +// +// } +// +//// @Test("This validates customer can be overridate later") func testWorkFlowOverridable2() async throws { +//// +//// class CustomHeaderConfig { +//// var enable = true +//// var headerValue = "iOS-SDK" +//// var headerName = "header-name" +//// } +//// +//// let customHeader = Module.of({ CustomHeaderConfig()}, priority: .low, block: { setup in +//// let config = setup.config +//// setup.next { ( context,_, request) in +//// if config.enable { +//// request.header(name: config.headerName, value: config.headerValue) +//// } +//// return request +//// } +//// +//// setup.start { ( context, request) in +//// if config.enable { +//// request.header(name: config.headerName, value: config.headerValue) +//// } +//// return request +//// } +//// +//// }) +//// +//// +//// let nosession = Module.of(priority: .high) { setup in +//// setup.next { ( context, _, request) in +//// request.header(name: "nosession", value: "true") +//// return request +//// } +//// } +//// +//// +//// let forceAuth = Module.of{ setup in +//// setup.start { ( context, request) in +//// request.header(name: "forceAuth", value: "true") +//// return request +//// } +//// } +//// +//// let workFlow = Workflow.config { config in +//// config.debug = true +//// config.timeout = 10 +//// +//// config.module(customHeader, false) { header in +//// header.enable = true +//// header.headerName = "header-name1" +//// header.headerValue = "Android-SDK1" +//// } +//// +//// config.module(customHeader) { header in +//// header.enable = true +//// header.headerName = "header-name2" +//// header.headerValue = "Android-SDK2" +//// } +//// +//// config.module(customHeader, false) { header in +//// header.enable = true +//// header.headerName = "header-name3" +//// header.headerValue = "iOS-SDK3" +//// } +//// +//// +//// config.module(customHeader) { header in +//// header.enable = true +//// header.headerName = "header-name4" +//// header.headerValue = "Android-SDK4" +//// } +//// +//// +//// config.module(forceAuth) +//// config.module(nosession) +//// +//// } +//// +//// try #require(workFlow.workFlowConfig.modules.count > 0) +//// +//// #expect(workFlow.workFlowConfig.modules.count == 5) +//// #expect(workFlow.workFlowConfig.highPriorityModule.count == 1) +//// #expect(workFlow.workFlowConfig.lowPriorityModule.count == 4) +//// +//// } +//// +// +//} diff --git a/Orchestrate/OrchestrateTests/RequestTests.swift b/Orchestrate/OrchestrateTests/RequestTests.swift new file mode 100644 index 0000000..5c18f10 --- /dev/null +++ b/Orchestrate/OrchestrateTests/RequestTests.swift @@ -0,0 +1,100 @@ +// +// RequestTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingOrchestrate + +final class RequestTests: XCTestCase { + + func testInit() async throws { + let request = Request() + XCTAssertNotNil(request) + } + + func testDefaultValues() async throws { + let request = Request() + let body = ["bodykey": "bodyvalue"] + let json = try? JSONSerialization.data(withJSONObject: body, options: []) + request.url("https://pingone.com") + request.header(name: "testHeader", value: "testValue") + request.header(name: "testHeader1", value: "testValue2") + request.header(name: "testHeader1", value: "testValue2") + request.parameter(name: "key1", value: "key1Value") + request.parameter(name: "key2", value: "key2Value") + request.body(body: body) + XCTAssertTrue(request.urlRequest.httpBody == json) + XCTAssertTrue(request.urlRequest.allHTTPHeaderFields?.count == 3) + XCTAssertTrue(request.urlRequest.url?.absoluteString == "https://pingone.com?key1=key1Value&key2=key2Value") + } + + func testDefaultValuesFormUrlEncoded() async throws { + let request = Request() + let body = ["bodykey": "bodyvalue"] + let jsonData = "bodykey=bodyvalue".data(using: .utf8) + request.url("https://pingone.com") + request.header(name: "testHeader", value: "testValue") + request.header(name: "testHeader1", value: "testValue2") + request.header(name: "testHeader1", value: "testValue2") + request.parameter(name: "key1", value: "key1Value") + request.parameter(name: "key2", value: "key2Value") + request.form(formData: body) + XCTAssertTrue(request.urlRequest.httpBody == jsonData) + XCTAssertTrue(request.urlRequest.allHTTPHeaderFields?.count == 3) + XCTAssertTrue(request.urlRequest.url?.absoluteString == "https://pingone.com?key1=key1Value&key2=key2Value") + } + + func testUrlSetsTheCorrectUrl() { + let request = Request() + request.url("http://example.com") + XCTAssertEqual("http://example.com", request.urlRequest.url?.absoluteString) + } + + func testParameterAppendsTheCorrectParameter() { + let request = Request() + request.parameter(name: "key", value: "value") + XCTAssertEqual("value", request.urlRequest.url?.query?.components(separatedBy: "&").first(where: { $0.contains("key=") })?.split(separator: "=")[1]) + } + + func testHeaderAppendsTheCorrectHeader() { + let request = Request() + request.header(name: "Content-Type", value: "application/json") + XCTAssertEqual("application/json", request.urlRequest.allHTTPHeaderFields?["Content-Type"]) + } + + func testCookiesSetsTheCorrectCookies() { + let request = Request() + let setCookie: [String: String] = ["Set-Cookie":"interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Max-Age=3600; Path=/; Expires=Thu, 09 May 2024 21:38:44 GMT; HttpOnly; Secure, interactionToken=abc; Max-Age=3600; Path=/; Expires=Thu, 09 May 2024 21:38:44 GMT; HttpOnly; Secure"] + let httpCookies = HTTPCookie.cookies(withResponseHeaderFields: setCookie, for: URL(string: "https://openam.example.com")!) + request.cookies(cookies: httpCookies) + + let cookieHeader = request.urlRequest.allHTTPHeaderFields?["Cookie"] ?? "" + XCTAssertTrue(cookieHeader.contains("interactionId")) + XCTAssertTrue(cookieHeader.contains("interactionToken")) + } + + func testBodySetsTheCorrectBody() { + let request = Request() + let jsonData = try! JSONSerialization.data(withJSONObject: ["key": "value"], options: []) + let body = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any] + request.body(body: body) + XCTAssertEqual(String(data: request.urlRequest.httpBody!, encoding: .utf8), """ + {"key":"value"} + """) + } + + func testFormSetsTheCorrectFormData() { + let request = Request() + let body = ["key": "value"] + request.form(formData: body) + XCTAssertEqual(String(data: request.urlRequest.httpBody!, encoding: .utf8), "key=value") + } +} diff --git a/Orchestrate/OrchestrateTests/ResponseTests.swift b/Orchestrate/OrchestrateTests/ResponseTests.swift new file mode 100644 index 0000000..4ef4054 --- /dev/null +++ b/Orchestrate/OrchestrateTests/ResponseTests.swift @@ -0,0 +1,93 @@ +// +// ResponseTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOrchestrate + +final class ResponseTests: XCTestCase { + + func testInit() async throws { + let response = Response(data: Data(), response: URLResponse()) + XCTAssertNotNil(response.data) + XCTAssertNotNil(response.response) + } + + func testDefaultValues() async throws { + let data = Data("{}".utf8) + let response = Response(data: data, response: URLResponse()) + XCTAssertNotNil(response.body()) + XCTAssertNotNil(try response.json(data: data)) + } + + func testHeader() async throws { + let data = Data("{}".utf8) + + let setCookie: [String: String] = ["Set-Cookie":"Ping=token; Expires=Wed, 21 Oct 1999 01:00:00 GMT; Domain=openam.example.com"] + + let urlresponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: setCookie)! + + let response = Response(data: data, response: urlresponse) + + XCTAssertEqual(response.header(name: "Set-Cookie"), "Ping=token; Expires=Wed, 21 Oct 1999 01:00:00 GMT; Domain=openam.example.com") + + XCTAssertEqual(response.status(), 200) + XCTAssertNotNil(try response.json(data: data)) + + XCTAssertEqual(response.getCookies().count, 1) + + } + + func testBodyShouldReturnResponseBodyAsString() { + let responseBody = "response body".data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: nil)! + let response = Response(data: responseBody, response: urlResponse) + + XCTAssertEqual(response.body(), "response body") + } + + func testStatusShouldReturnResponseStatusCode() { + let responseBody = "response body".data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: nil)! + let response = Response(data: responseBody, response: urlResponse) + + XCTAssertEqual(response.status(), 200) + } + + func testCookiesShouldReturnCookiesFromResponse() { + let responseBody = "response body".data(using: .utf8)! + let setCookie: [String: String] = ["Set-Cookie":"cookie1=value1, cookie2=value2"] + let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: setCookie)! + let response = Response(data: responseBody, response: urlResponse) + + XCTAssertEqual(response.getCookies().count, 2) + XCTAssertEqual(response.getCookies().first?.name, "cookie1") + XCTAssertEqual(response.getCookies().first?.value, "value1") + XCTAssertEqual(response.getCookies().last?.name, "cookie2") + XCTAssertEqual(response.getCookies().last?.value, "value2") + XCTAssertEqual(response.header(name: "Set-Cookie"), "cookie1=value1, cookie2=value2") + } + + func testHeaderShouldReturnSpecificHeaderValue() { + let responseBody = "response body".data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: ["Content-Type": "application/json"])! + let response = Response(data: responseBody, response: urlResponse) + + XCTAssertEqual(response.header(name: "Content-Type"), "application/json") + } + + func testHeaderShouldReturnNullIfHeaderIsNotPresent() { + let responseBody = "response body".data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: nil)! + let response = Response(data: responseBody, response: urlResponse) + + XCTAssertNil(response.header(name: "Non-Existent-Header")) + } +} diff --git a/Orchestrate/OrchestrateTests/SessionTests.swift b/Orchestrate/OrchestrateTests/SessionTests.swift new file mode 100644 index 0000000..293f933 --- /dev/null +++ b/Orchestrate/OrchestrateTests/SessionTests.swift @@ -0,0 +1,31 @@ +// +// SessionTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingOrchestrate + +final class SessionTests: XCTestCase { + + func testEmptySessionValueShouldReturnEmptyString() { + XCTAssertEqual("", EmptySession().value) + } + + func testSessionValueShouldReturnCorrectSessionValue() { + let session = MockSession() + XCTAssertEqual("session_value", session.value) + } + + class MockSession: Session { + var value: String = "session_value" + } + +} diff --git a/Orchestrate/OrchestrateTests/WorkflowTests.swift b/Orchestrate/OrchestrateTests/WorkflowTests.swift new file mode 100644 index 0000000..ecceea4 --- /dev/null +++ b/Orchestrate/OrchestrateTests/WorkflowTests.swift @@ -0,0 +1,666 @@ +// +// WorkflowTests.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingOrchestrate + +class CustomHeaderConfig { + var enable = true + var headerValue = "iOS-SDK" + var headerName = "header-name" +} + +class WorkflowTest: XCTestCase { + + var customHeader = Module.of({ CustomHeaderConfig() }, setup: { setup in + let config = setup.config + setup.next { ( context, _, request) in + if config.enable { + request.header(name: config.headerName, value: config.headerValue) + } + return request + } + + setup.start { ( context, request) in + if config.enable { + request.header(name: config.headerName, value: config.headerValue) + } + return request + } + }) + + let nosession = Module.of { setup in + setup.next { ( context,_, request) in + request.header(name: "nosession", value: "true") + return request + } + } + + + let forceAuth = Module.of { setup in + setup.start { ( context, request) in + request.header(name: "forceAuth", value: "true") + return request + } + } + + override func setUp() { + super.setUp() + MockURLProtocol.startInterceptingRequests() + } + + override func tearDown() { + super.tearDown() + MockURLProtocol.stopInterceptingRequests() + } + + func testSameModuleInstanceShouldOverrideExistingModule() { + + let workflow = Workflow.createWorkflow { config in + config.timeout = 10 + + config.module(forceAuth) + + config.module(customHeader) { header in + header.headerName = "header-name1" + header.headerValue = "Android-SDK" + } + + config.module(nosession) + + config.module(customHeader) { header in + header.headerName = "header-name2" + header.headerValue = "iOS-SDK" + } + } + + XCTAssertEqual(workflow.config.modules.count, 3) + let moduleRegistry = workflow.config.modules.first(where: { $0.id == customHeader.id }) + XCTAssertEqual((moduleRegistry?.config as? CustomHeaderConfig)?.headerValue, "iOS-SDK") + XCTAssertEqual(forceAuth.id, workflow.config.modules[0].id) + XCTAssertEqual(customHeader.id, workflow.config.modules[1].id) + XCTAssertEqual(nosession.id, workflow.config.modules[2].id) + + } + + func testSameModuleInstanceShouldOverrideExistingModuleWithPriority (){ + let workflow = Workflow.createWorkflow { config in + config.timeout = 10 + + config.module(forceAuth, 1) + + config.module(customHeader, 2) { header in + header.headerName = "header-name1" + header.headerValue = "Android-SDK" + } + + config.module(nosession, 3) + + config.module(customHeader, 10) { header in + header.headerName = "header-name2" + header.headerValue = "iOS-SDK" + } + } + + XCTAssertEqual(workflow.config.modules.count, 3) + let moduleRegistry = workflow.config.modules.first(where: { $0.id == customHeader.id }) + XCTAssertEqual((moduleRegistry?.config as? CustomHeaderConfig)?.headerValue, "iOS-SDK") + let index = workflow.config.modules.firstIndex(where: { $0.id == customHeader.id }) + //The original position is retained even with the new priority + XCTAssertEqual(index, 1) + + } + + func testNotOverrideExistingModule() { + let workflow = Workflow.createWorkflow { config in + config.timeout = 10 + + config.module(forceAuth) + config.module(nosession) + + config.module(customHeader) { header in + header.headerName = "header-name1" + header.headerValue = "Android-SDK" + } + // You cannot register 2 modules with the same instance, + // the later one will replace the previous one + // To register the same module twice, you need to add overridable = false + // APPEND means the module cannot be replaced and does not replace previous module + config.module(customHeader, mode: .append) { header in + header.headerName = "header-name2" + header.headerValue = "iOS-SDK" + } + } + + XCTAssertEqual(workflow.config.modules.count, 4) + } + + func testNotOverrideExistingModuleWithIgnore() { + let workflow = Workflow.createWorkflow { config in + config.timeout = 10 + + config.module(forceAuth) + config.module(nosession) + + config.module(customHeader) { header in + header.headerName = "header-name1" + header.headerValue = "Android-SDK" + } + config.module(customHeader, mode: .ignore) { header in + header.headerName = "header-name2" + header.headerValue = "iOS-SDK" + } + } + + XCTAssertEqual(workflow.config.modules.count, 3) + let moduleRegistry = workflow.config.modules.first(where: { $0.id == customHeader.id }) + XCTAssertEqual((moduleRegistry?.config as? CustomHeaderConfig)?.headerValue, "Android-SDK") + } + + func testAddModuleWithIgnore() { + let workflow = Workflow.createWorkflow { config in + config.timeout = 10 + + config.module(forceAuth) + config.module(nosession) + + config.module(customHeader, mode: .ignore) { header in + header.headerName = "header-name2" + header.headerValue = "iOS-SDK" + } + } + + XCTAssertEqual(workflow.config.modules.count, 3) + let moduleRegistry = workflow.config.modules.first(where: { $0.id == customHeader.id }) + XCTAssertEqual((moduleRegistry?.config as? CustomHeaderConfig)?.headerValue, "iOS-SDK") + } + + func testModuleDefaultPriority() { + let workflow = Workflow.createWorkflow { config in + config.timeout = 10 + + config.module(forceAuth) + config.module(nosession) + + config.module(customHeader) { header in + header.headerName = "header-name2" + header.headerValue = "iOS-SDK" + } + + config.module(nosession) + } + + XCTAssertEqual(workflow.config.modules.count, 3) + XCTAssertEqual(forceAuth.id, workflow.config.modules[0].id) + XCTAssertEqual(nosession.id, workflow.config.modules[1].id) + XCTAssertEqual(customHeader.id, workflow.config.modules[2].id) + } + + func testModuleCustomPriority() { + let workflow = Workflow.createWorkflow { config in + config.timeout = 10 + + config.module(forceAuth, 2) + config.module(nosession, 3) + + config.module(customHeader, 1) { header in + header.headerName = "header-name2" + header.headerValue = "iOS-SDK" + } + } + + XCTAssertEqual(workflow.config.modules.count, 3) + XCTAssertEqual(customHeader.id, workflow.config.modules[0].id) + XCTAssertEqual(forceAuth.id, workflow.config.modules[1].id) + XCTAssertEqual(nosession.id, workflow.config.modules[2].id) + } + + func testModuleWithSamePriority() { + let workflow = Workflow.createWorkflow { config in + config.timeout = 10 + + config.module(forceAuth, 2) + config.module(nosession, 2) + + config.module(customHeader, 1) { header in + header.headerName = "header-name2" + header.headerValue = "iOS-SDK" + } + + } + + XCTAssertEqual(workflow.config.modules.count, 3) + XCTAssertEqual(customHeader.id, workflow.config.modules[0].id) + XCTAssertEqual(forceAuth.id, workflow.config.modules[1].id) + XCTAssertEqual(nosession.id, workflow.config.modules[2].id) + } + + func testModuleRegistrationInEachStage() { + let workflow = Workflow(config: WorkflowConfig()) + XCTAssertTrue(workflow.config.modules.isEmpty) + XCTAssertTrue(workflow.initHandlers.isEmpty) + XCTAssertTrue(workflow.startHandlers.isEmpty) + XCTAssertTrue(workflow.nextHandlers.isEmpty) + XCTAssertTrue(workflow.responseHandlers.isEmpty) + XCTAssertTrue(workflow.nodeHandlers.isEmpty) + XCTAssertTrue(workflow.successHandlers.isEmpty) + XCTAssertTrue(workflow.signOffHandlers.isEmpty) + + let dummy = Module.of({CustomHeaderConfig()}) { module in + module.initialize { + // Intercept all send request and inject custom header + } + module.start { _, request in + return request + } + module.next { _,_, request in + return request + } + module.response {_,_ in + // Handle response + } + module.node { _, node in + return node + } + module.success { _, success in + return success + } + module.transform {_,_ in + return SuccessNode(session: EmptySession()) + } + module.signOff { signOff in + return signOff + } + } + + let workflow2 = Workflow.createWorkflow { config in + config.module(dummy) + } + + XCTAssertEqual(workflow2.config.modules.count, 1) + XCTAssertEqual(workflow2.initHandlers.count, 1) + XCTAssertEqual(workflow2.startHandlers.count, 1) + XCTAssertEqual(workflow2.nextHandlers.count, 1) + XCTAssertEqual(workflow2.responseHandlers.count, 1) + XCTAssertEqual(workflow2.nodeHandlers.count, 1) + XCTAssertEqual(workflow2.successHandlers.count, 1) + XCTAssertEqual(workflow2.signOffHandlers.count, 1) + } + + func testModuleExecution() async { + var initializeCnt = 0 + var startCnt = 0 + var nextCnt = 0 + var responseCnt = 0 + var transformCnt = 0 + var nodeReceivedCnt = 0 + var successCnt = 0 + var success = false + + let json: [String: Bool] = ["booleanKey": true] + + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data()) + } + + var workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + } + + let dummy = Module.of({CustomHeaderConfig()}) { module in + module.initialize { + initializeCnt += 1 + } + module.start { _, request in + startCnt += 1 + return request + } + module.next { _,_, request in + nextCnt += 1 + return request + } + module.response {_,_ in + responseCnt += 1 + // Handle response + } + module.node { _, node in + nodeReceivedCnt += 1 + return node + } + module.success { _, success1 in + successCnt += 1 + return success1 + } + module.transform { flowContext,_ in + transformCnt += 1 + if success { + return SuccessNode(session: EmptySession()) + } else { + success = true + return TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: []) + } + } + } + + workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(dummy) + } + + let node = await workflow.start() + let connector = node as! ContinueNode + XCTAssertTrue(workflow === connector.workflow) + XCTAssertEqual(json, connector.input as? [String: Bool]) + XCTAssertTrue(connector.actions.isEmpty) + let isEmpty = connector.context.flowContext.isEmpty + XCTAssertTrue(isEmpty) + _ = await connector.next() + XCTAssertEqual(1, initializeCnt) + XCTAssertEqual(1, startCnt) + XCTAssertEqual(1, nextCnt) + XCTAssertEqual(2, responseCnt) + XCTAssertEqual(2, transformCnt) + XCTAssertEqual(2, nodeReceivedCnt) + XCTAssertEqual(1, successCnt) + } + + func testAccessToWorkflowContext() async { + let json: [String: Any] = ["booleanKey": true] + var success = false + + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data()) + } + + var workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + } + + let dummy = Module.of({CustomHeaderConfig()}) { module in + module.initialize { + let count = (workflow.sharedContext.get(key: "count") as! Int) + 1 + workflow.sharedContext.set(key: "count", value: count) + } + module.start { _, request in + let count = (workflow.sharedContext.get(key: "count")as! Int) + 1 + workflow.sharedContext.set(key: "count", value: count) + return request + } + module.next { _,_, request in + let count = (workflow.sharedContext.get(key: "count")as! Int) + 1 + workflow.sharedContext.set(key: "count", value: count) + return request + } + module.response {_,_ in + let count = (workflow.sharedContext.get(key: "count")as! Int) + 1 + workflow.sharedContext.set(key: "count", value: count) + // Handle response + } + module.node { _, node in + let count = (workflow.sharedContext.get(key: "count")as! Int) + 1 + workflow.sharedContext.set(key: "count", value: count) + return node + } + module.success { _, success1 in + let count = (workflow.sharedContext.get(key: "count")as! Int) + 1 + workflow.sharedContext.set(key: "count", value: count) + return success1 + } + module.transform { flowContext,_ in + let count = (workflow.sharedContext.get(key: "count")as! Int) + 1 + workflow.sharedContext.set(key: "count", value: count) + if success { + return SuccessNode(session: EmptySession()) + } else { + success = true + return TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: []) + } + } + } + + workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(dummy) + } + + workflow.sharedContext.set(key: "count", value: 0) + + let node = await workflow.start() + _ = await (node as! ContinueNode).next() + + let count = (workflow.sharedContext.get(key: "count")as! Int) + XCTAssertEqual(10, count) + } + + func testInitStateFunctionThrowException() async { + let initFailed = Module.of({CustomHeaderConfig()}) { module in + module.initialize { + throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"]) + } + } + + let workflow = Workflow.createWorkflow { config in + config.module(initFailed) + } + + let node = await workflow.start() + XCTAssertTrue(node is FailureNode) + XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize") + } + + func testStartStateFunctionThrowsException() async throws { + let startFailed = Module.of({CustomHeaderConfig()}) { module in + module.start { _,_ in + throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"]) + } + } + let workflow = Workflow.createWorkflow { config in + config.module(startFailed) + } + + let node = await workflow.start() + XCTAssertTrue(node is FailureNode) + XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize") + } + + func testResponseStateFunctionThrowsException() async throws { + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data()) + } + + let responseFailed = Module.of({CustomHeaderConfig()}) { module in + module.response { _,_ in + throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"]) + } + } + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(responseFailed) + } + + let node = await workflow.start() + XCTAssertTrue(node is FailureNode) + XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize") + } + + func testTransformStateFunctionThrowsException() async throws { + + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data()) + } + + let transformFailed = Module.of({CustomHeaderConfig()}) { module in + module.transform { _,_ in + throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"]) + } + } + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(transformFailed) + } + + let node = await workflow.start() + XCTAssertTrue(node is FailureNode) + XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize") + } + + func testNextStateFunctionThrowsException() async throws { + let json = ["booleanKey": true] + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data()) + } + var workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + } + + let nextFailed = Module.of({CustomHeaderConfig()}) { module in + module.next { _,_, _ in + throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"]) + } + + module.transform { flowContext,_ in + TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: []) + } + } + + workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(nextFailed) + } + + let node = await workflow.start() + let next = await (node as? ContinueNode)?.next() + XCTAssertTrue(next is FailureNode) + XCTAssertEqual((next as! FailureNode).cause.localizedDescription, "Failed to initialize") + } + + func testNodeStateFunctionThrowsException() async throws { + let json = ["booleanKey": true] + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data()) + } + var workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + } + + let nodeFailed = Module.of({CustomHeaderConfig()}) { module in + module.node { _, _ in + throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"]) + } + + module.transform { flowContext,_ in + TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: []) + } + } + + workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(nodeFailed) + } + + let node = await workflow.start() + XCTAssertTrue(node is FailureNode) + XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize") + } + + func testSuccessStateFunctionThrowsException() async throws { + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data()) + } + var workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + } + + let successFailed = Module.of({CustomHeaderConfig()}) { module in + module.success { _, _ in + throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"]) + } + + module.transform { flowContext,_ in + SuccessNode(session: EmptySession()) + } + } + + workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(successFailed) + } + + let node = await workflow.start() + XCTAssertTrue(node is FailureNode) + XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize") + } + + func testExecutionFailure() async { + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 400, httpVersion: nil, headerFields: nil)!, "Invalid request".data(using: .utf8)!) + } + + let dummy = Module.of({CustomHeaderConfig()}) { module in + module.transform {_,_ in + return ErrorNode(input: [:], message: "Invalid request") + } + } + + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + config.module(dummy) + } + + let node = await workflow.start() + let failure = node as! ErrorNode + XCTAssertEqual("Invalid request", failure.message) + XCTAssertTrue(failure.input.isEmpty) + } + + func testSignOffShouldReturnSuccessResultWhenNoExceptionsOccur() async { + + MockURLProtocol.requestHandler = { request in + return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, "".data(using: .utf8)!) + } + + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + } + + let result = await workflow.signOff() + + switch result { + case .success(let result): + XCTAssertNotNil(result) + break + + default: + XCTFail("Expected success result") + } + + } + + func testSignOffShouldReturnFailureResultWhenExceptionOccurs() async { + MockURLProtocol.requestHandler = { request in + throw NSError(domain: "SignOffError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Sign off failed"]) + } + + let workflow = Workflow.createWorkflow { config in + config.httpClient = HttpClient(session: .shared) + } + + let result = await workflow.signOff() + switch result { + case .failure(let error): + XCTAssertEqual(error.localizedDescription, "Sign off failed") + break + + default: + XCTFail("Expected failure result") + } + } +} diff --git a/Orchestrate/OrchestrateTests/mock/MockAPIEndpoint.swift b/Orchestrate/OrchestrateTests/mock/MockAPIEndpoint.swift new file mode 100644 index 0000000..0aa8910 --- /dev/null +++ b/Orchestrate/OrchestrateTests/mock/MockAPIEndpoint.swift @@ -0,0 +1,43 @@ +// +// MockAPIEndpoint.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +enum MockAPIEndpoint { + static let baseURL = "https://auth.test-one-pingone.com" + + case authorization + case token + case userinfo + case endSession + case revocation + case discovery + case customHTMLTemplate + + var url: URL { + switch self { + case .authorization: + return URL(string: "\(MockAPIEndpoint.baseURL)/authorize")! + case .token: + return URL(string: "\(MockAPIEndpoint.baseURL)/token")! + case .userinfo: + return URL(string: "\(MockAPIEndpoint.baseURL)/userinfo")! + case .endSession: + return URL(string: "\(MockAPIEndpoint.baseURL)/signoff")! + case .revocation: + return URL(string: "\(MockAPIEndpoint.baseURL)/revoke")! + case .discovery: + return URL(string: "\(MockAPIEndpoint.baseURL)/.well-known/openid-configuration")! + case .customHTMLTemplate: + return URL(string: "\(MockAPIEndpoint.baseURL)/customHTMLTemplate")! + } + } +} diff --git a/Orchestrate/OrchestrateTests/mock/MockURLProtocol.swift b/Orchestrate/OrchestrateTests/mock/MockURLProtocol.swift new file mode 100644 index 0000000..23cedb6 --- /dev/null +++ b/Orchestrate/OrchestrateTests/mock/MockURLProtocol.swift @@ -0,0 +1,58 @@ +// +// MockURLProtocol.swift +// OrchestrateTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest + +class MockURLProtocol: URLProtocol { + public static var requestHistory: [URLRequest] = [URLRequest]() + + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + static func startInterceptingRequests() { + URLProtocol.registerClass(MockURLProtocol.self) + } + + static func stopInterceptingRequests() { + URLProtocol.unregisterClass(MockURLProtocol.self) + requestHistory.removeAll() + } + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + MockURLProtocol.requestHistory.append(request) + + guard let handler = MockURLProtocol.requestHandler else { + XCTFail("Received unexpected request with no handler set") + return + } + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() { + + } +} + diff --git a/Orchestrate/README.md b/Orchestrate/README.md new file mode 100644 index 0000000..3c38899 --- /dev/null +++ b/Orchestrate/README.md @@ -0,0 +1,126 @@ +

+ + Logo + +


+

+ +# PingOrchestrate + +## Overview + +PingOrchestrate provides a simple way to build a state machine for ForgeRock Journey and PingOne DaVinci. +It allows you to define a series of states and transitions between them. You can use the workflow engine to build +complex workflows that involve multiple steps and conditions. +The Workflow engine allows you to define a series of functions and register them as a module to the `Workflow` instance +in different state of the workflow. + +The Workflow engine contains the following states: + + + +| State | Description | Input | Output | Use Case | +|------------|:--------------------------------------:|---------:|--------:|---------------------------------------------------------------------------:| +| Init | Initialize the modules | () | Void | OAuth module loads endpoint from discovery URL | +| Start | Start the flow | Request | Request | Intercept the start request, for example inject forceAuth parameter | +| Response | Handle the response | Response | Void | Parse the response, for example store the cookie header. | +| *Transform | Transform the response to Node | Response | Node | For Journey, transform response to Callback, for DaVinci transform to Form | +| Node | Process the Node or Transform the Node | Node | Node | Transform MetadataCallback to WebAuthnCallback | +| Next | Move to next state | Request | Request | Inject noSession parameter | +| Success | Flow finished with Success | Success | Success | Prepare Success with AuthCode or Session | +| SignOff | SignOff the Workflow | Request | Request | Revoke Token and end session | + +### Module + +Module allows you to register functions in different state of the workflow. For example, you can register a function +that +will be called when the workflow is initialized, +when a node is received, when a node is sent, and when the workflow is started. + + + +Information can be shared across state, there are 2 contexts + +| Context | Scope | Access | +|-----------------|:-----------------------------------:|-----------------------------------:| +| WorkflowContext | Workflow Instance | ```context["name"] = "value" ``` | +| FlowContext | Flow from Start to Finish (Success) | ```flowContext["name"]= "value"``` | + +## Integrating the SDK into your project + +Use Cocoapods or Swift Package Manger + +## Usage + +To use the `Workflow` class, you need to create an instance of it by passing a configuration block to the `createWorkflow` method. The +configuration block allows you to register various modules of the `Workflow` instance. + +Here's an example of how to create a `Workflow` instance: + +```swift +let workflow = Workflow.createWorkflow { config in + config.module(forceAuth) + config.module(noSession) + config.module(session) +} +_ = await workflow.start() +``` +The `start` method returns a `Node` instance. The `Node` class represents the current state of the application. You can +use the `next` method to transition to the next state. + +### SignOff +There is a special state called `SignOff` that is used to sign off the user. You can use the `signOff` method to sign off +the user. + +```swift + _ = await workflow.signOff() +``` + +## Custom Module + +You can provide a custom module to the `Workflow` instance. A module is a class that uses the `Module` interface. +The `Module` interface allows the module to install `function`s in different states during the `Workflow` flow. + +```swift +let customHeader = Module.of({ CustomHeaderConfig1() }, setup: { setup in + let config = setup.config + // Intercept all send request and inject custom header + setup.next { ( context, _, request) in + if config.enable { + request.header(name: config.headerName, value: config.headerValue) + } + return request + } +}) +``` + +You can then install the custom module in the `Workflow` configuration block like this: + +```swift +let workflow = Workflow.createWorkflow { config in + config.module(customHeader) { header in + header.headerName = "iOS-SDK2" + header.headerValue = "headervalue3" + } +} +``` + +More module examples: +```swift +let nosession = Module.of { setup in +//Intercept all send request and inject custom header during start state + setup.next { ( context,_, request) in + request.header(name: "nosession", value: "true") + return request + } +} + + +let forceAuth = Module.of { setup in +//Intercept all send request and inject custom header during start state + setup.start { ( context, request) in + request.header(name: "forceAuth", value: "true") + return request + } +} +``` diff --git a/Orchestrate/images/functions.png b/Orchestrate/images/functions.png new file mode 100644 index 0000000..2e51fe4 Binary files /dev/null and b/Orchestrate/images/functions.png differ diff --git a/Orchestrate/images/state.png b/Orchestrate/images/state.png new file mode 100644 index 0000000..f0250a2 Binary files /dev/null and b/Orchestrate/images/state.png differ diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..74341bc --- /dev/null +++ b/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.3 +import PackageDescription + +let package = Package ( + name: "Ping-SDK-iOS", + platforms: [ + .iOS(.v13) + ], + products: [ + .library(name: "PingLogger", targets: ["PingLogger"]), + .library(name: "PingStorage", targets: ["PingStorage"]), + .library(name: "PingOrchestrate", targets: ["PingOrchestrate"]), + .library(name: "PingOidc", targets: ["PingOidc"]), + .library(name: "PingDavinci", targets: ["PingDavinci"]) + ], + dependencies: [ + ], + targets: [ + .target(name: "PingLogger", dependencies: [], path: "Logger/Logger", exclude: ["Logger.h"], resources: [.copy("PrivacyInfo.xcprivacy")]), + .target(name: "PingStorage", dependencies: [], path: "Storage/Storage", exclude: ["Storage.h"], resources: [.copy("PrivacyInfo.xcprivacy")]), + .target(name: "PingOrchestrate", dependencies: [.target(name: "PingLogger"), .target(name: "PingStorage")], path: "Orchestrate/Orchestrate", exclude: ["Orchestrate.h"], resources: [.copy("PrivacyInfo.xcprivacy")]), + .target(name: "PingOidc", dependencies: [.target(name: "PingOrchestrate")], path: "Oidc/Oidc", exclude: ["Oidc.h"], resources: [.copy("PrivacyInfo.xcprivacy")]), + .target(name: "PingDavinci", dependencies: [.target(name: "PingOidc"),], path: "Davinci/Davinci", exclude: ["Davinci.h"], resources: [.copy("PrivacyInfo.xcprivacy")]), + ] +) diff --git a/PingDavinci.podspec b/PingDavinci.podspec new file mode 100644 index 0000000..ac88dee --- /dev/null +++ b/PingDavinci.podspec @@ -0,0 +1,39 @@ +# +# Be sure to run `pod lib lint PingDavinci.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'PingDavinci' + s.version = '1.0.0' + s.summary = 'PingDavinci SDK for iOS' + s.description = <<-DESC + The PingDavinci SDK is a powerful and flexible library for Authentication and Authorization. It is designed to be easy to use and extensible. It provides a simple API for navigating the authentication flow and handling the various states that can +occur during the authentication process. + DESC + s.homepage = 'https://www.pingidentity.com/' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = 'Ping Identity' + + s.source = { + :git => 'https://github.com/ForgeRock/ping-ios-sdk.git', + :tag => s.version.to_s + } + + s.module_name = 'PingDavinci' + s.swift_versions = ['5.0', '5.1'] + + s.ios.deployment_target = '13.0' + + base_dir = "Davinci/Davinci" + s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.c', base_dir + '/**/*.h' + s.resource_bundles = { + 'Davinci' => [base_dir + '/*.xcprivacy'] + } + + s.ios.dependency 'PingOidc', '~> 1.0.0' + +end diff --git a/PingLogger.podspec b/PingLogger.podspec new file mode 100644 index 0000000..66bb921 --- /dev/null +++ b/PingLogger.podspec @@ -0,0 +1,35 @@ +# +# Be sure to run `pod lib lint PingLogger.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'PingLogger' + s.version = '1.0.0' + s.summary = 'PingLogger SDK for iOS' + s.description = <<-DESC + The PingLogger SDK provides a versatile logging interface and a set of common loggers for the Ping SDKs. + DESC + s.homepage = 'https://www.pingidentity.com/' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = 'Ping Identity' + + s.source = { + :git => 'https://github.com/ForgeRock/ping-ios-sdk.git', + :tag => s.version.to_s + } + + s.module_name = 'PingLogger' + s.swift_versions = ['5.0', '5.1'] + + s.ios.deployment_target = '13.0' + + base_dir = "Logger/Logger" + s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.h' + s.resource_bundles = { + 'Logger' => [base_dir + '/*.xcprivacy'] + } +end diff --git a/PingOidc.podspec b/PingOidc.podspec new file mode 100644 index 0000000..02ee2b1 --- /dev/null +++ b/PingOidc.podspec @@ -0,0 +1,37 @@ +# +# Be sure to run `pod lib lint PingOidc.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'PingOidc' + s.version = '1.0.0' + s.summary = 'PingOidc SDK for iOS' + s.description = <<-DESC + The PingOidc SDK provides OIDC client for PingOne and ForgeRock platform. + DESC + s.homepage = 'https://www.pingidentity.com/' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = 'Ping Identity' + + s.source = { + :git => 'https://github.com/ForgeRock/ping-ios-sdk.git', + :tag => s.version.to_s + } + + s.module_name = 'PingOidc' + s.swift_versions = ['5.0', '5.1'] + + s.ios.deployment_target = '13.0' + + base_dir = "Oidc/Oidc" + s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.h' + s.resource_bundles = { + 'Oidc' => [base_dir + '/*.xcprivacy'] + } + + s.ios.dependency 'PingOrchestrate', '~> 1.0.0' +end diff --git a/PingOrchestrate.podspec b/PingOrchestrate.podspec new file mode 100644 index 0000000..afe2433 --- /dev/null +++ b/PingOrchestrate.podspec @@ -0,0 +1,38 @@ +# +# Be sure to run `pod lib lint PingOrchestrate.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'PingOrchestrate' + s.version = '1.0.0' + s.summary = 'PingOrchestrate SDK for iOS' + s.description = <<-DESC + The PingOrchestrate SDK provides a simple way to build a state machine for ForgeRock Journey and PingOne DaVinci. + DESC + s.homepage = 'https://www.pingidentity.com/' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = 'Ping Identity' + + s.source = { + :git => 'https://github.com/ForgeRock/ping-ios-sdk.git', + :tag => s.version.to_s + } + + s.module_name = 'PingOrchestrate' + s.swift_versions = ['5.0', '5.1'] + + s.ios.deployment_target = '13.0' + + base_dir = "Orchestrate/Orchestrate" + s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.h' + s.resource_bundles = { + 'Orchestrate' => [base_dir + '/*.xcprivacy'] + } + + s.ios.dependency 'PingLogger', '~> 1.0.0' + s.ios.dependency 'PingStorage', '~> 1.0.0' +end diff --git a/PingStorage.podspec b/PingStorage.podspec new file mode 100644 index 0000000..b4ecc5f --- /dev/null +++ b/PingStorage.podspec @@ -0,0 +1,35 @@ +# +# Be sure to run `pod lib lint PingStorage.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'PingStorage' + s.version = '1.0.0' + s.summary = 'PingStorage SDK for iOS' + s.description = <<-DESC + The PingStorage SDK provides a flexible storage interface and a set of common storage solutions for the Ping SDKs. + DESC + s.homepage = 'https://www.pingidentity.com/' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = 'Ping Identity' + + s.source = { + :git => 'https://github.com/ForgeRock/ping-ios-sdk.git', + :tag => s.version.to_s + } + + s.module_name = 'PingStorage' + s.swift_versions = ['5.0', '5.1'] + + s.ios.deployment_target = '13.0' + + base_dir = "Storage/Storage" + s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.c', base_dir + '/**/*.h' + s.resource_bundles = { + 'Storage' => [base_dir + '/*.xcprivacy'] + } +end diff --git a/PingTestHost/PingTestHost.xcodeproj/project.pbxproj b/PingTestHost/PingTestHost.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3b37e25 --- /dev/null +++ b/PingTestHost/PingTestHost.xcodeproj/project.pbxproj @@ -0,0 +1,450 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A507A2672CEBED9F007F5C16 /* PingDavinci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2622CEBED9F007F5C16 /* PingDavinci.framework */; }; + A507A2682CEBED9F007F5C16 /* PingDavinci.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2622CEBED9F007F5C16 /* PingDavinci.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A507A2692CEBED9F007F5C16 /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2632CEBED9F007F5C16 /* PingLogger.framework */; }; + A507A26A2CEBED9F007F5C16 /* PingLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2632CEBED9F007F5C16 /* PingLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A507A26B2CEBED9F007F5C16 /* PingOidc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2642CEBED9F007F5C16 /* PingOidc.framework */; }; + A507A26C2CEBED9F007F5C16 /* PingOidc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2642CEBED9F007F5C16 /* PingOidc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A507A26D2CEBED9F007F5C16 /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2652CEBED9F007F5C16 /* PingOrchestrate.framework */; }; + A507A26E2CEBED9F007F5C16 /* PingOrchestrate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2652CEBED9F007F5C16 /* PingOrchestrate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A507A26F2CEBED9F007F5C16 /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2662CEBED9F007F5C16 /* PingStorage.framework */; }; + A507A2702CEBED9F007F5C16 /* PingStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2662CEBED9F007F5C16 /* PingStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A54BF4692BF2E33C00CD24D4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF4682BF2E33C00CD24D4 /* AppDelegate.swift */; }; + A54BF46B2BF2E33C00CD24D4 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF46A2BF2E33C00CD24D4 /* SceneDelegate.swift */; }; + A54BF46D2BF2E33C00CD24D4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF46C2BF2E33C00CD24D4 /* ViewController.swift */; }; + A54BF4702BF2E33C00CD24D4 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = A54BF46F2BF2E33C00CD24D4 /* Base */; }; + A54BF4722BF2E34000CD24D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A54BF4712BF2E34000CD24D4 /* Assets.xcassets */; }; + A54BF4752BF2E34000CD24D4 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = A54BF4742BF2E34000CD24D4 /* Base */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A507A2712CEBED9F007F5C16 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + A507A26A2CEBED9F007F5C16 /* PingLogger.framework in Embed Frameworks */, + A507A26C2CEBED9F007F5C16 /* PingOidc.framework in Embed Frameworks */, + A507A2682CEBED9F007F5C16 /* PingDavinci.framework in Embed Frameworks */, + A507A2702CEBED9F007F5C16 /* PingStorage.framework in Embed Frameworks */, + A507A26E2CEBED9F007F5C16 /* PingOrchestrate.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + A507A2622CEBED9F007F5C16 /* PingDavinci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingDavinci.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A507A2632CEBED9F007F5C16 /* PingLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A507A2642CEBED9F007F5C16 /* PingOidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOidc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A507A2652CEBED9F007F5C16 /* PingOrchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOrchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A507A2662CEBED9F007F5C16 /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A50966F02C50522300A4E5B5 /* PingOidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOidc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A51D4CE62C656D7D00FE09E0 /* PingOrchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOrchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A51D4CE82C656D7D00FE09E0 /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A51D4CEA2C656D8E00FE09E0 /* PingDavinci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingDavinci.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A54BF4652BF2E33C00CD24D4 /* PingTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PingTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A54BF4682BF2E33C00CD24D4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A54BF46A2BF2E33C00CD24D4 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + A54BF46C2BF2E33C00CD24D4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + A54BF46F2BF2E33C00CD24D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + A54BF4712BF2E34000CD24D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A54BF4742BF2E34000CD24D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + A54BF4762BF2E34000CD24D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A54BF4832BF2E5B800CD24D4 /* PingLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A57A08972CE40FDE006F0CBB /* Storage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Storage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A57A089D2CE412E8006F0CBB /* Logger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Logger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A57A08BB2CE4FD96006F0CBB /* Orchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Orchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A57A08C22CE50537006F0CBB /* Oidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Oidc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A57A08DE2CE50937006F0CBB /* Davinci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Davinci.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A5D1CD5E2BF504E400D1C83E /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A54BF4622BF2E33C00CD24D4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A507A2692CEBED9F007F5C16 /* PingLogger.framework in Frameworks */, + A507A26B2CEBED9F007F5C16 /* PingOidc.framework in Frameworks */, + A507A2672CEBED9F007F5C16 /* PingDavinci.framework in Frameworks */, + A507A26F2CEBED9F007F5C16 /* PingStorage.framework in Frameworks */, + A507A26D2CEBED9F007F5C16 /* PingOrchestrate.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A54BF45C2BF2E33C00CD24D4 = { + isa = PBXGroup; + children = ( + A54BF4672BF2E33C00CD24D4 /* PingTestHost */, + A54BF4662BF2E33C00CD24D4 /* Products */, + A54BF4822BF2E5B800CD24D4 /* Frameworks */, + ); + sourceTree = ""; + }; + A54BF4662BF2E33C00CD24D4 /* Products */ = { + isa = PBXGroup; + children = ( + A54BF4652BF2E33C00CD24D4 /* PingTestHost.app */, + ); + name = Products; + sourceTree = ""; + }; + A54BF4672BF2E33C00CD24D4 /* PingTestHost */ = { + isa = PBXGroup; + children = ( + A54BF4682BF2E33C00CD24D4 /* AppDelegate.swift */, + A54BF46A2BF2E33C00CD24D4 /* SceneDelegate.swift */, + A54BF46C2BF2E33C00CD24D4 /* ViewController.swift */, + A54BF46E2BF2E33C00CD24D4 /* Main.storyboard */, + A54BF4712BF2E34000CD24D4 /* Assets.xcassets */, + A54BF4732BF2E34000CD24D4 /* LaunchScreen.storyboard */, + A54BF4762BF2E34000CD24D4 /* Info.plist */, + ); + path = PingTestHost; + sourceTree = ""; + }; + A54BF4822BF2E5B800CD24D4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A507A2622CEBED9F007F5C16 /* PingDavinci.framework */, + A507A2632CEBED9F007F5C16 /* PingLogger.framework */, + A507A2642CEBED9F007F5C16 /* PingOidc.framework */, + A507A2652CEBED9F007F5C16 /* PingOrchestrate.framework */, + A507A2662CEBED9F007F5C16 /* PingStorage.framework */, + A57A08DE2CE50937006F0CBB /* Davinci.framework */, + A57A08C22CE50537006F0CBB /* Oidc.framework */, + A57A08BB2CE4FD96006F0CBB /* Orchestrate.framework */, + A57A089D2CE412E8006F0CBB /* Logger.framework */, + A57A08972CE40FDE006F0CBB /* Storage.framework */, + A51D4CEA2C656D8E00FE09E0 /* PingDavinci.framework */, + A51D4CE62C656D7D00FE09E0 /* PingOrchestrate.framework */, + A51D4CE82C656D7D00FE09E0 /* PingStorage.framework */, + A50966F02C50522300A4E5B5 /* PingOidc.framework */, + A5D1CD5E2BF504E400D1C83E /* PingStorage.framework */, + A54BF4832BF2E5B800CD24D4 /* PingLogger.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A54BF4642BF2E33C00CD24D4 /* PingTestHost */ = { + isa = PBXNativeTarget; + buildConfigurationList = A54BF4792BF2E34000CD24D4 /* Build configuration list for PBXNativeTarget "PingTestHost" */; + buildPhases = ( + A54BF4612BF2E33C00CD24D4 /* Sources */, + A54BF4622BF2E33C00CD24D4 /* Frameworks */, + A54BF4632BF2E33C00CD24D4 /* Resources */, + A507A2712CEBED9F007F5C16 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PingTestHost; + productName = PingTestHost; + productReference = A54BF4652BF2E33C00CD24D4 /* PingTestHost.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A54BF45D2BF2E33C00CD24D4 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + A54BF4642BF2E33C00CD24D4 = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = A54BF4602BF2E33C00CD24D4 /* Build configuration list for PBXProject "PingTestHost" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A54BF45C2BF2E33C00CD24D4; + productRefGroup = A54BF4662BF2E33C00CD24D4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A54BF4642BF2E33C00CD24D4 /* PingTestHost */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A54BF4632BF2E33C00CD24D4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A54BF4722BF2E34000CD24D4 /* Assets.xcassets in Resources */, + A54BF4752BF2E34000CD24D4 /* Base in Resources */, + A54BF4702BF2E33C00CD24D4 /* Base in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A54BF4612BF2E33C00CD24D4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A54BF46D2BF2E33C00CD24D4 /* ViewController.swift in Sources */, + A54BF4692BF2E33C00CD24D4 /* AppDelegate.swift in Sources */, + A54BF46B2BF2E33C00CD24D4 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + A54BF46E2BF2E33C00CD24D4 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + A54BF46F2BF2E33C00CD24D4 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + A54BF4732BF2E34000CD24D4 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + A54BF4742BF2E34000CD24D4 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + A54BF4772BF2E34000CD24D4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A54BF4782BF2E34000CD24D4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A54BF47A2BF2E34000CD24D4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PingTestHost/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingTestHost; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Debug; + }; + A54BF47B2BF2E34000CD24D4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PingTestHost/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingTestHost; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A54BF4602BF2E33C00CD24D4 /* Build configuration list for PBXProject "PingTestHost" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A54BF4772BF2E34000CD24D4 /* Debug */, + A54BF4782BF2E34000CD24D4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A54BF4792BF2E34000CD24D4 /* Build configuration list for PBXNativeTarget "PingTestHost" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A54BF47A2BF2E34000CD24D4 /* Debug */, + A54BF47B2BF2E34000CD24D4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A54BF45D2BF2E33C00CD24D4 /* Project object */; +} diff --git a/PingTestHost/PingTestHost.xcodeproj/xcshareddata/IDETemplateMacros.plist b/PingTestHost/PingTestHost.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..16cf018 --- /dev/null +++ b/PingTestHost/PingTestHost.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,17 @@ + + + + + FILEHEADER + +// ___FILENAME___ +// ___PACKAGENAME___ +// +// Copyright (c) ___YEAR___ Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + + diff --git a/PingTestHost/PingTestHost.xcodeproj/xcshareddata/xcschemes/PingTestHost.xcscheme b/PingTestHost/PingTestHost.xcodeproj/xcshareddata/xcschemes/PingTestHost.xcscheme new file mode 100644 index 0000000..90ba3ae --- /dev/null +++ b/PingTestHost/PingTestHost.xcodeproj/xcshareddata/xcschemes/PingTestHost.xcscheme @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PingTestHost/PingTestHost.xctestplan b/PingTestHost/PingTestHost.xctestplan new file mode 100644 index 0000000..3cf159d --- /dev/null +++ b/PingTestHost/PingTestHost.xctestplan @@ -0,0 +1,60 @@ +{ + "configurations" : [ + { + "id" : "653B9364-499B-4DFE-A77C-201EEBA63A50", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:PingTestHost.xcodeproj", + "identifier" : "A54BF4642BF2E33C00CD24D4", + "name" : "PingTestHost" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:..\/Logger\/Logger.xcodeproj", + "identifier" : "A5A796B52BE04E68004D0F2D", + "name" : "LoggerTests" + } + }, + { + "target" : { + "containerPath" : "container:..\/Storage\/Storage.xcodeproj", + "identifier" : "A5A797032BE1782F004D0F2D", + "name" : "StorageTests" + } + }, + { + "target" : { + "containerPath" : "container:..\/Oidc\/Oidc.xcodeproj", + "identifier" : "3AB1CA0E2BD6F99C003FCE3C", + "name" : "OidcTests" + } + }, + { + "target" : { + "containerPath" : "container:..\/Orchestrate\/Orchestrate.xcodeproj", + "identifier" : "3A54417D2BCDF1D900385131", + "name" : "OrchestrateTests" + } + }, + { + "skippedTests" : [ + "DaVinciIntegrationTests\/testHappyPathWithPasswordCredentials()", + "DaVinciIntegrationTests\/testInvalidPassword()" + ], + "target" : { + "containerPath" : "container:..\/Davinci\/Davinci.xcodeproj", + "identifier" : "3A5441A82BCDF20700385131", + "name" : "DavinciTests" + } + } + ], + "version" : 1 +} diff --git a/PingTestHost/PingTestHost/AppDelegate.swift b/PingTestHost/PingTestHost/AppDelegate.swift new file mode 100644 index 0000000..e6a6f85 --- /dev/null +++ b/PingTestHost/PingTestHost/AppDelegate.swift @@ -0,0 +1,39 @@ +// +// AppDelegate.swift +// PingTestHost +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/PingTestHost/PingTestHost/Assets.xcassets/AccentColor.colorset/Contents.json b/PingTestHost/PingTestHost/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/PingTestHost/PingTestHost/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PingTestHost/PingTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json b/PingTestHost/PingTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/PingTestHost/PingTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PingTestHost/PingTestHost/Assets.xcassets/Contents.json b/PingTestHost/PingTestHost/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/PingTestHost/PingTestHost/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PingTestHost/PingTestHost/Base.lproj/LaunchScreen.storyboard b/PingTestHost/PingTestHost/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/PingTestHost/PingTestHost/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PingTestHost/PingTestHost/Base.lproj/Main.storyboard b/PingTestHost/PingTestHost/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/PingTestHost/PingTestHost/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PingTestHost/PingTestHost/Info.plist b/PingTestHost/PingTestHost/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/PingTestHost/PingTestHost/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/PingTestHost/PingTestHost/SceneDelegate.swift b/PingTestHost/PingTestHost/SceneDelegate.swift new file mode 100644 index 0000000..2dccc5a --- /dev/null +++ b/PingTestHost/PingTestHost/SceneDelegate.swift @@ -0,0 +1,55 @@ +// +// SceneDelegate.swift +// PingTestHost +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/PingTestHost/PingTestHost/ViewController.swift b/PingTestHost/PingTestHost/ViewController.swift new file mode 100644 index 0000000..cf921f1 --- /dev/null +++ b/PingTestHost/PingTestHost/ViewController.swift @@ -0,0 +1,22 @@ +// +// ViewController.swift +// PingTestHost +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/README.md b/README.md index 65a1659..c655664 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ -# unified-sdk-ios -Initial Repo for the Unified SDK - iOS +[![Build](https://github.com/ForgeRock/unified-sdk-ios/actions/workflows/ci.yaml/badge.svg)](https://github.com/ForgeRock/unified-sdk-ios/actions/workflows/ci.yaml) + +

+ + Logo + +


+

+ +The Ping SDK for iOS is designed for creating mobile native apps that seamlessly integrate with the PingOne platform. +It offers a range of APIs for user authentication, user device management, and accessing resources secured by PingOne. + + + + + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details diff --git a/SampleApps/Ping.xcworkspace/contents.xcworkspacedata b/SampleApps/Ping.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..ebf3bea --- /dev/null +++ b/SampleApps/Ping.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/SampleApps/Ping.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SampleApps/Ping.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/SampleApps/Ping.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj b/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a0dba91 --- /dev/null +++ b/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj @@ -0,0 +1,893 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 63; + objects = { + +/* Begin PBXBuildFile section */ + 3A3709962BEB038200AA7B51 /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3709952BEB038200AA7B51 /* AccessToken.swift */; }; + 3A5441332BCDF10900385131 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441322BCDF10900385131 /* ContentView.swift */; }; + 3A5441352BCDF10B00385131 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A5441342BCDF10B00385131 /* Assets.xcassets */; }; + 3A5441382BCDF10B00385131 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A5441372BCDF10B00385131 /* Preview Assets.xcassets */; }; + 3A5441422BCDF10B00385131 /* PingExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441412BCDF10B00385131 /* PingExampleTests.swift */; }; + 3A54414C2BCDF10B00385131 /* PingExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A54414B2BCDF10B00385131 /* PingExampleUITests.swift */; }; + 3A54414E2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A54414D2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift */; }; + 3A8092F42BE99F0F00AD667F /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8092F32BE99F0F00AD667F /* InputView.swift */; }; + 3A8092F62BE9BB6B00AD667F /* TokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8092F52BE9BB6B00AD667F /* TokenViewModel.swift */; }; + 3A8532332BE2D5D800F8619D /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8532322BE2D5D800F8619D /* LoginView.swift */; }; + 3AB1C9F42BD6C6F3003FCE3C /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1C9F32BD6C6F3003FCE3C /* LoginViewModel.swift */; }; + 3ACD90E12BE586BC00DABCE6 /* DavinciViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */; }; + 3AE945742BF27224007B3381 /* LoginUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE945732BF27223007B3381 /* LoginUtility.swift */; }; + A5560CA02CFE4EDA0076CA8D /* PingDavinci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A091A2CE511E7006F0CBB /* PingDavinci.framework */; }; + A5560CA12CFE4EDA0076CA8D /* PingDavinci.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A091A2CE511E7006F0CBB /* PingDavinci.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A5560CA22CFE4EDA0076CA8D /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A08FA2CE50D51006F0CBB /* PingLogger.framework */; }; + A5560CA32CFE4EDA0076CA8D /* PingLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A08FA2CE50D51006F0CBB /* PingLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A5560CA42CFE4EDA0076CA8D /* PingOidc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A09122CE51162006F0CBB /* PingOidc.framework */; }; + A5560CA52CFE4EDA0076CA8D /* PingOidc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A09122CE51162006F0CBB /* PingOidc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A5560CA62CFE4EDA0076CA8D /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A090A2CE50FA5006F0CBB /* PingOrchestrate.framework */; }; + A5560CA72CFE4EDA0076CA8D /* PingOrchestrate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A090A2CE50FA5006F0CBB /* PingOrchestrate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A5560CA82CFE4EDA0076CA8D /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A09022CE50EF1006F0CBB /* PingStorage.framework */; }; + A5560CA92CFE4EDA0076CA8D /* PingStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A09022CE50EF1006F0CBB /* PingStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A588D0062BF6A0E500F9052A /* StorageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A588D0042BF6A0E500F9052A /* StorageViewModel.swift */; }; + A588D0072BF6A0E500F9052A /* LoggerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3A54413E2BCDF10B00385131 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A5441252BCDF10900385131 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A54412C2BCDF10900385131; + remoteInfo = PingExample; + }; + 3A5441482BCDF10B00385131 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A5441252BCDF10900385131 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A54412C2BCDF10900385131; + remoteInfo = PingExample; + }; + A57A08F92CE50D51006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = A5A796AE2BE04E67004D0F2D; + remoteInfo = Logger; + }; + A57A08FB2CE50D51006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = A5A796B62BE04E68004D0F2D; + remoteInfo = LoggerTests; + }; + A57A09012CE50EF1006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A588CFF92BF69F3300F9052A /* Storage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = A5A796FA2BE1782E004D0F2D; + remoteInfo = Storage; + }; + A57A09032CE50EF1006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A588CFF92BF69F3300F9052A /* Storage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = A5A797042BE1782F004D0F2D; + remoteInfo = StorageTests; + }; + A57A09092CE50FA5006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3A5441742BCDF1D900385131; + remoteInfo = Orchestrate; + }; + A57A090B2CE50FA5006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3A54417E2BCDF1D900385131; + remoteInfo = OrchestrateTests; + }; + A57A09112CE51162006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3AB1CA072BD6F99C003FCE3C; + remoteInfo = Oidc; + }; + A57A09132CE51162006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3AB1CA0F2BD6F99C003FCE3C; + remoteInfo = OidcTests; + }; + A57A09192CE511E7006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3A5441A12BCDF20700385131; + remoteInfo = Davinci; + }; + A57A091B2CE511E7006F0CBB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3A5441A92BCDF20700385131; + remoteInfo = DavinciTests; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 3A203D872BDB1A900020C995 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + A5560CA32CFE4EDA0076CA8D /* PingLogger.framework in Embed Frameworks */, + A5560CA52CFE4EDA0076CA8D /* PingOidc.framework in Embed Frameworks */, + A5560CA12CFE4EDA0076CA8D /* PingDavinci.framework in Embed Frameworks */, + A5560CA92CFE4EDA0076CA8D /* PingStorage.framework in Embed Frameworks */, + A5560CA72CFE4EDA0076CA8D /* PingOrchestrate.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3A3709952BEB038200AA7B51 /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = ""; }; + 3A54412D2BCDF10900385131 /* PingExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PingExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A5441322BCDF10900385131 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 3A5441342BCDF10B00385131 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3A5441372BCDF10B00385131 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 3A54413D2BCDF10B00385131 /* PingExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PingExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A5441412BCDF10B00385131 /* PingExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingExampleTests.swift; sourceTree = ""; }; + 3A5441472BCDF10B00385131 /* PingExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PingExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A54414B2BCDF10B00385131 /* PingExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingExampleUITests.swift; sourceTree = ""; }; + 3A54414D2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingExampleUITestsLaunchTests.swift; sourceTree = ""; }; + 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Davinci.xcodeproj; path = ../../Davinci/Davinci.xcodeproj; sourceTree = ""; }; + 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Orchestrate.xcodeproj; path = ../../Orchestrate/Orchestrate.xcodeproj; sourceTree = ""; }; + 3A8092F32BE99F0F00AD667F /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; }; + 3A8092F52BE9BB6B00AD667F /* TokenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenViewModel.swift; sourceTree = ""; }; + 3A8532322BE2D5D800F8619D /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; + 3AB1C9F32BD6C6F3003FCE3C /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Oidc.xcodeproj; path = ../../Oidc/Oidc.xcodeproj; sourceTree = ""; }; + 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DavinciViewModel.swift; sourceTree = ""; }; + 3AE945732BF27223007B3381 /* LoginUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginUtility.swift; sourceTree = ""; }; + A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Logger.xcodeproj; path = ../../Logger/Logger.xcodeproj; sourceTree = ""; }; + A588CFF92BF69F3300F9052A /* Storage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Storage.xcodeproj; path = ../../Storage/Storage.xcodeproj; sourceTree = ""; }; + A588D0042BF6A0E500F9052A /* StorageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageViewModel.swift; sourceTree = ""; }; + A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggerViewModel.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3A54412A2BCDF10900385131 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5560CA22CFE4EDA0076CA8D /* PingLogger.framework in Frameworks */, + A5560CA42CFE4EDA0076CA8D /* PingOidc.framework in Frameworks */, + A5560CA02CFE4EDA0076CA8D /* PingDavinci.framework in Frameworks */, + A5560CA82CFE4EDA0076CA8D /* PingStorage.framework in Frameworks */, + A5560CA62CFE4EDA0076CA8D /* PingOrchestrate.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A54413A2BCDF10B00385131 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A5441442BCDF10B00385131 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3A5441242BCDF10900385131 = { + isa = PBXGroup; + children = ( + A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */, + A588CFF92BF69F3300F9052A /* Storage.xcodeproj */, + 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */, + 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */, + 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */, + 3A54412F2BCDF10900385131 /* PingExample */, + 3A5441402BCDF10B00385131 /* PingExampleTests */, + 3A54414A2BCDF10B00385131 /* PingExampleUITests */, + 3A54412E2BCDF10900385131 /* Products */, + 3A5441642BCDF17600385131 /* Frameworks */, + ); + sourceTree = ""; + }; + 3A54412E2BCDF10900385131 /* Products */ = { + isa = PBXGroup; + children = ( + 3A54412D2BCDF10900385131 /* PingExample.app */, + 3A54413D2BCDF10B00385131 /* PingExampleTests.xctest */, + 3A5441472BCDF10B00385131 /* PingExampleUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 3A54412F2BCDF10900385131 /* PingExample */ = { + isa = PBXGroup; + children = ( + 3AE945732BF27223007B3381 /* LoginUtility.swift */, + 3A5441322BCDF10900385131 /* ContentView.swift */, + 3A8532322BE2D5D800F8619D /* LoginView.swift */, + 3A3709952BEB038200AA7B51 /* AccessToken.swift */, + 3AB1C9F32BD6C6F3003FCE3C /* LoginViewModel.swift */, + 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */, + 3A8092F52BE9BB6B00AD667F /* TokenViewModel.swift */, + 3A8092F32BE99F0F00AD667F /* InputView.swift */, + A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */, + A588D0042BF6A0E500F9052A /* StorageViewModel.swift */, + 3A5441342BCDF10B00385131 /* Assets.xcassets */, + 3A5441362BCDF10B00385131 /* Preview Content */, + ); + path = PingExample; + sourceTree = ""; + }; + 3A5441362BCDF10B00385131 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 3A5441372BCDF10B00385131 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 3A5441402BCDF10B00385131 /* PingExampleTests */ = { + isa = PBXGroup; + children = ( + 3A5441412BCDF10B00385131 /* PingExampleTests.swift */, + ); + path = PingExampleTests; + sourceTree = ""; + }; + 3A54414A2BCDF10B00385131 /* PingExampleUITests */ = { + isa = PBXGroup; + children = ( + 3A54414B2BCDF10B00385131 /* PingExampleUITests.swift */, + 3A54414D2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift */, + ); + path = PingExampleUITests; + sourceTree = ""; + }; + 3A5441642BCDF17600385131 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + A57A08F52CE50D51006F0CBB /* Products */ = { + isa = PBXGroup; + children = ( + A57A08FA2CE50D51006F0CBB /* PingLogger.framework */, + A57A08FC2CE50D51006F0CBB /* LoggerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + A57A08FD2CE50EF1006F0CBB /* Products */ = { + isa = PBXGroup; + children = ( + A57A09022CE50EF1006F0CBB /* PingStorage.framework */, + A57A09042CE50EF1006F0CBB /* StorageTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + A57A09052CE50FA5006F0CBB /* Products */ = { + isa = PBXGroup; + children = ( + A57A090A2CE50FA5006F0CBB /* PingOrchestrate.framework */, + A57A090C2CE50FA5006F0CBB /* OrchestrateTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + A57A090D2CE51162006F0CBB /* Products */ = { + isa = PBXGroup; + children = ( + A57A09122CE51162006F0CBB /* PingOidc.framework */, + A57A09142CE51162006F0CBB /* OidcTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + A57A09152CE511E7006F0CBB /* Products */ = { + isa = PBXGroup; + children = ( + A57A091A2CE511E7006F0CBB /* PingDavinci.framework */, + A57A091C2CE511E7006F0CBB /* DavinciTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3A54412C2BCDF10900385131 /* PingExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A5441512BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExample" */; + buildPhases = ( + 3A5441292BCDF10900385131 /* Sources */, + 3A54412A2BCDF10900385131 /* Frameworks */, + 3A54412B2BCDF10900385131 /* Resources */, + 3A203D872BDB1A900020C995 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PingExample; + productName = PingExample; + productReference = 3A54412D2BCDF10900385131 /* PingExample.app */; + productType = "com.apple.product-type.application"; + }; + 3A54413C2BCDF10B00385131 /* PingExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A5441542BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExampleTests" */; + buildPhases = ( + 3A5441392BCDF10B00385131 /* Sources */, + 3A54413A2BCDF10B00385131 /* Frameworks */, + 3A54413B2BCDF10B00385131 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3A54413F2BCDF10B00385131 /* PBXTargetDependency */, + ); + name = PingExampleTests; + productName = PingExampleTests; + productReference = 3A54413D2BCDF10B00385131 /* PingExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 3A5441462BCDF10B00385131 /* PingExampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A5441572BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExampleUITests" */; + buildPhases = ( + 3A5441432BCDF10B00385131 /* Sources */, + 3A5441442BCDF10B00385131 /* Frameworks */, + 3A5441452BCDF10B00385131 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3A5441492BCDF10B00385131 /* PBXTargetDependency */, + ); + name = PingExampleUITests; + productName = PingExampleUITests; + productReference = 3A5441472BCDF10B00385131 /* PingExampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3A5441252BCDF10900385131 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + 3A54412C2BCDF10900385131 = { + CreatedOnToolsVersion = 15.0; + }; + 3A54413C2BCDF10B00385131 = { + CreatedOnToolsVersion = 15.0; + TestTargetID = 3A54412C2BCDF10900385131; + }; + 3A5441462BCDF10B00385131 = { + CreatedOnToolsVersion = 15.0; + TestTargetID = 3A54412C2BCDF10900385131; + }; + }; + }; + buildConfigurationList = 3A5441282BCDF10900385131 /* Build configuration list for PBXProject "PingExample" */; + compatibilityVersion = "Xcode 15.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3A5441242BCDF10900385131; + productRefGroup = 3A54412E2BCDF10900385131 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = A57A09152CE511E7006F0CBB /* Products */; + ProjectRef = 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */; + }, + { + ProductGroup = A57A08F52CE50D51006F0CBB /* Products */; + ProjectRef = A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */; + }, + { + ProductGroup = A57A090D2CE51162006F0CBB /* Products */; + ProjectRef = 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */; + }, + { + ProductGroup = A57A09052CE50FA5006F0CBB /* Products */; + ProjectRef = 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */; + }, + { + ProductGroup = A57A08FD2CE50EF1006F0CBB /* Products */; + ProjectRef = A588CFF92BF69F3300F9052A /* Storage.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 3A54412C2BCDF10900385131 /* PingExample */, + 3A54413C2BCDF10B00385131 /* PingExampleTests */, + 3A5441462BCDF10B00385131 /* PingExampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + A57A08FA2CE50D51006F0CBB /* PingLogger.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = PingLogger.framework; + remoteRef = A57A08F92CE50D51006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A08FC2CE50D51006F0CBB /* LoggerTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = LoggerTests.xctest; + remoteRef = A57A08FB2CE50D51006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A09022CE50EF1006F0CBB /* PingStorage.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = PingStorage.framework; + remoteRef = A57A09012CE50EF1006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A09042CE50EF1006F0CBB /* StorageTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = StorageTests.xctest; + remoteRef = A57A09032CE50EF1006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A090A2CE50FA5006F0CBB /* PingOrchestrate.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = PingOrchestrate.framework; + remoteRef = A57A09092CE50FA5006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A090C2CE50FA5006F0CBB /* OrchestrateTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OrchestrateTests.xctest; + remoteRef = A57A090B2CE50FA5006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A09122CE51162006F0CBB /* PingOidc.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = PingOidc.framework; + remoteRef = A57A09112CE51162006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A09142CE51162006F0CBB /* OidcTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OidcTests.xctest; + remoteRef = A57A09132CE51162006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A091A2CE511E7006F0CBB /* PingDavinci.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = PingDavinci.framework; + remoteRef = A57A09192CE511E7006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + A57A091C2CE511E7006F0CBB /* DavinciTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = DavinciTests.xctest; + remoteRef = A57A091B2CE511E7006F0CBB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 3A54412B2BCDF10900385131 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A5441382BCDF10B00385131 /* Preview Assets.xcassets in Resources */, + 3A5441352BCDF10B00385131 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A54413B2BCDF10B00385131 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A5441452BCDF10B00385131 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3A5441292BCDF10900385131 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A588D0072BF6A0E500F9052A /* LoggerViewModel.swift in Sources */, + 3A8092F62BE9BB6B00AD667F /* TokenViewModel.swift in Sources */, + 3ACD90E12BE586BC00DABCE6 /* DavinciViewModel.swift in Sources */, + 3AE945742BF27224007B3381 /* LoginUtility.swift in Sources */, + A588D0062BF6A0E500F9052A /* StorageViewModel.swift in Sources */, + 3A8532332BE2D5D800F8619D /* LoginView.swift in Sources */, + 3A5441332BCDF10900385131 /* ContentView.swift in Sources */, + 3A8092F42BE99F0F00AD667F /* InputView.swift in Sources */, + 3AB1C9F42BD6C6F3003FCE3C /* LoginViewModel.swift in Sources */, + 3A3709962BEB038200AA7B51 /* AccessToken.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A5441392BCDF10B00385131 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A5441422BCDF10B00385131 /* PingExampleTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A5441432BCDF10B00385131 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A54414C2BCDF10B00385131 /* PingExampleUITests.swift in Sources */, + 3A54414E2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3A54413F2BCDF10B00385131 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A54412C2BCDF10900385131 /* PingExample */; + targetProxy = 3A54413E2BCDF10B00385131 /* PBXContainerItemProxy */; + }; + 3A5441492BCDF10B00385131 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A54412C2BCDF10900385131 /* PingExample */; + targetProxy = 3A5441482BCDF10B00385131 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 3A54414F2BCDF10B00385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = ""; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 3A5441502BCDF10B00385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = ""; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3A5441522BCDF10B00385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"PingExample/Preview Content\""; + DEVELOPMENT_TEAM = 9QSE66762D; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Davinci; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3A5441532BCDF10B00385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"PingExample/Preview Content\""; + DEVELOPMENT_TEAM = 9QSE66762D; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Davinci; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3A5441552BCDF10B00385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PingExample"; + }; + name = Debug; + }; + 3A5441562BCDF10B00385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PingExample"; + }; + name = Release; + }; + 3A5441582BCDF10B00385131 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = PingExample; + }; + name = Debug; + }; + 3A5441592BCDF10B00385131 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = PingExample; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3A5441282BCDF10900385131 /* Build configuration list for PBXProject "PingExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A54414F2BCDF10B00385131 /* Debug */, + 3A5441502BCDF10B00385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A5441512BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A5441522BCDF10B00385131 /* Debug */, + 3A5441532BCDF10B00385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A5441542BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A5441552BCDF10B00385131 /* Debug */, + 3A5441562BCDF10B00385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A5441572BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A5441582BCDF10B00385131 /* Debug */, + 3A5441592BCDF10B00385131 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3A5441252BCDF10900385131 /* Project object */; +} diff --git a/SampleApps/PingExample/PingExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SampleApps/PingExample/PingExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/SampleApps/PingExample/PingExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/IDETemplateMacros.plist b/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..16cf018 --- /dev/null +++ b/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,17 @@ + + + + + FILEHEADER + +// ___FILENAME___ +// ___PACKAGENAME___ +// +// Copyright (c) ___YEAR___ Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + + diff --git a/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/xcschemes/PingExample.xcscheme b/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/xcschemes/PingExample.xcscheme new file mode 100644 index 0000000..99ff12f --- /dev/null +++ b/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/xcschemes/PingExample.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampleApps/PingExample/PingExample/AccessToken.swift b/SampleApps/PingExample/PingExample/AccessToken.swift new file mode 100644 index 0000000..762a351 --- /dev/null +++ b/SampleApps/PingExample/PingExample/AccessToken.swift @@ -0,0 +1,41 @@ +// +// AccessToken.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI + +struct AccessTokenView: View { + + @StateObject var accessToken = TokenViewModel() + + var body: some View { + VStack { + ScrollView { + Text($accessToken.accessToken.wrappedValue) + .foregroundStyle(.secondary) + .padding(.horizontal) + .navigationTitle("AccessToken") + } + } + } +} + +struct UserInfoView: View { + + @StateObject var userInfoViewModel = UserInfoViewModel() + + var body: some View { + ScrollView { + Text($userInfoViewModel.userInfo.wrappedValue) + .foregroundStyle(.secondary) + .padding(.horizontal) + .navigationTitle("User Info") + } + } +} diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/AccentColor.colorset/Contents.json b/SampleApps/PingExample/PingExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..514e171 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Screenshot 2024-05-09-01 1.jpg", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Screenshot 2024-05-09-01 1.jpg b/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Screenshot 2024-05-09-01 1.jpg new file mode 100644 index 0000000..8cfc7d4 Binary files /dev/null and b/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Screenshot 2024-05-09-01 1.jpg differ diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/Contents.json b/SampleApps/PingExample/PingExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Contents.json b/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Contents.json new file mode 100644 index 0000000..88d4d3d --- /dev/null +++ b/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Screenshot 2024-05-09 at 12.00.43 PM.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Screenshot 2024-05-09 at 12.00.43\342\200\257PM.png" "b/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Screenshot 2024-05-09 at 12.00.43\342\200\257PM.png" new file mode 100644 index 0000000..9451c65 Binary files /dev/null and "b/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Screenshot 2024-05-09 at 12.00.43\342\200\257PM.png" differ diff --git a/SampleApps/PingExample/PingExample/ContentView.swift b/SampleApps/PingExample/PingExample/ContentView.swift new file mode 100644 index 0000000..13ada42 --- /dev/null +++ b/SampleApps/PingExample/PingExample/ContentView.swift @@ -0,0 +1,161 @@ +import SwiftUI + + +struct ContentView: View { + + + @State private var startDavinici = false + + @State private var path: [String] = [] + + var body: some View { + NavigationStack(path: $path) { + List { + NavigationLink(value: "Davinci") { + Text("Launch Davinci") + } + NavigationLink(value: "Token") { + Text("Access Token") + } + NavigationLink(value: "User") { + Text("User Info") + } + + NavigationLink(value: "Logout") { + Text("Logout") + } + + NavigationLink(value: "Logger") { + Text("Logger") + } + NavigationLink(value: "Storage") { + Text("Storage") + } + }.navigationDestination(for: String.self) { item in + switch item { + case "Davinci": + DavinciView(path: $path) + case "Token": + AccessTokenView() + case "User": + UserInfoView() + case "Logout": + LogOutView(path: $path) + case "Logger": + LoggerView() + case "Storage": + StorageView() + default: + EmptyView() + } + }.navigationBarTitle("Ping Davinci") + } + } +} + + +struct LogOutView: View { + + @Binding var path: [String] + + @StateObject private var viewmodel = LogOutViewModel() + + var body: some View { + + Text("Logout") + .font(.title) + .navigationBarTitle("Logout", displayMode: .inline) + + NextButton(title: "Procced to logout") { + Task { + await viewmodel.logout() + path.removeLast() + path.append("Davinci") + } + } + + } +} + + +struct SecondTabView: View { + var body: some View { + Text("LogOut") + .font(.title) + .navigationBarTitle("LogOut", displayMode: .inline) + + } +} + +struct Register: View { + var body: some View { + Text("Register") + .font(.title) + .navigationBarTitle("Register", displayMode: .inline) + } +} + +struct ForgotPassword: View { + var body: some View { + Text("ForgotPassword") + .font(.title) + .navigationBarTitle("ForgotPassword", displayMode: .inline) + } +} + +struct LoggerView: View { + var loggerViewModel = LoggerViewModel() + var body: some View { + Text("This View is for testing Logger functionality.\nPlease check the Console Logs") + .font(.title3) + .multilineTextAlignment(.center) + .navigationBarTitle("Logger", displayMode: .inline) + .onAppear() { + loggerViewModel.setupLogger() + } + } +} + +struct StorageView: View { + var storageViewModel = StorageViewModel() + var body: some View { + Text("This View is for testing Storage functionality.\nPlease check the Console Logs") + .font(.title3) + .multilineTextAlignment(.center) + .navigationBarTitle("Storage", displayMode: .inline) + .onAppear() { + Task { + await storageViewModel.setupMemoryStorage() + await storageViewModel.setupKeychainStorage() + } + } + } +} + +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} + +struct ActivityIndicatorView: View { + @Binding var isAnimating: Bool + let style: UIActivityIndicatorView.Style + let color: Color + + var body: some View { + if isAnimating { + VStack { + Spacer() + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: color)) + .padding() + Spacer() + } + .background(Color.black.opacity(0.4).ignoresSafeArea()) + } + } +} diff --git a/SampleApps/PingExample/PingExample/DavinciViewModel.swift b/SampleApps/PingExample/PingExample/DavinciViewModel.swift new file mode 100644 index 0000000..4974883 --- /dev/null +++ b/SampleApps/PingExample/PingExample/DavinciViewModel.swift @@ -0,0 +1,103 @@ +// +// DavinciViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import PingDavinci +import PingOidc +import PingOrchestrate +import PingLogger +import PingStorage + +public let davinciStage = DaVinci.createDaVinci { config in + //config.debug = true + + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "2dde00e2-3dd5-42b1-96e9-ad17e29f4bbd" + oidcValue.scopes = ["openid", "email", "address", "phone", "profile", "ttl"] + oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" + oidcValue.discoveryEndpoint = "https://auth.test-one-pingone.com/0c6851ed-0f12-4c9a-a174-9b1bf8b438ae/as/.well-known/openid-configuration" + } +} + +public let davinciTest = DaVinci.createDaVinci { config in + //config.debug = true + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1" + oidcValue.scopes = ["openid", "email", "address", "phone", "profile"] + oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" + oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration" + } +} + +public let davinciProd = DaVinci.createDaVinci { config in + //config.debug = true + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = "2dde00e2-3dd5-42b1-96e9-ad17e29f4bbd" + oidcValue.scopes = ["openid", "email", "address", "phone", "profile", "ttl"] + oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" + oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration" + } +} + +// Change this to Prod/Stage +public let davinci = davinciProd + +class DavinciViewModel: ObservableObject { + + @Published public var data: StateNode = StateNode() + + @Published public var isLoading: Bool = false + + init() { + + Task { + await startDavinci() + } + } + + + private func startDavinci() async { + + await MainActor.run { + isLoading = true + } + + let node = await davinci.start() + + await MainActor.run { + self.data = StateNode(currentNode: node, previousNode: node) + isLoading = false + } + + } + + public func next(node: Node) async { + await MainActor.run { + isLoading = true + } + if let nextNode = node as? ContinueNode { + let next = await nextNode.next() + await MainActor.run { + self.data = StateNode(currentNode: next, previousNode: node) + isLoading = false + } + } + } +} + +class StateNode { + var currentNode: Node? = nil + var previousNode: Node? = nil + + init(currentNode: Node? = nil, previousNode: Node? = nil) { + self.currentNode = currentNode + self.previousNode = previousNode + } +} diff --git a/SampleApps/PingExample/PingExample/InputView.swift b/SampleApps/PingExample/PingExample/InputView.swift new file mode 100644 index 0000000..c5f5f9e --- /dev/null +++ b/SampleApps/PingExample/PingExample/InputView.swift @@ -0,0 +1,110 @@ +// +// InputView.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingDavinci + +struct InputView: View { + @State var text: String = "" + let placeholderString: String + var secureField: Bool = false + let field: any Collector + + var body: some View { + VStack(alignment: .leading) { + if secureField { + SecureField(placeholderString, text: $text) + .padding() + .background(Color.themeTextField) + .textContentType(.oneTimeCode) + .disableAutocorrection(true) + .autocapitalization(.none) // Set autocapitalization to none + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) + .onChange(of: text) { newValue in + if let field = field as? PasswordCollector { + field.value = newValue + } + } + } else { + TextField(placeholderString, text: $text) + .padding() + .background(Color.themeTextField) + .disableAutocorrection(true) + .textContentType(.oneTimeCode) + .autocapitalization(.none) // Set autocapitalization to none + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) + .onChange(of: text) { newValue in + + if let field = field as? TextCollector { + field.value = newValue + } + + + } + } + + + }.padding([.leading, .trailing], 27.5) + } +} + +struct InputButton: View { + let title: String + let field: any Collector + let action: () -> (Void) + var body: some View { + Button(action: { + + if let field = field as? SubmitCollector { + field.value = field.key + } + + action() + } ) { + Text(title) + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) + .background(Color(red: 163.0/255.0, green: 19.0/255.0, blue: 0.0/255.0)) // Red color + .cornerRadius(15.0) + .shadow(radius: 10.0, x: 20, y: 10) + } + + } +} + +struct NextButton: View { + let title: String + let action: () -> (Void) + var body: some View { + Button(action: { + action() + } ) { + Text(title) + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) + .background(Color.green) + .cornerRadius(15.0) + .shadow(radius: 10.0, x: 20, y: 10) + } + + } +} + +extension Color { + static var themeTextField: Color { + return Color(red: 220.0/255.0, green: 230.0/255.0, blue: 230.0/255.0, opacity: 1.0) + } +} diff --git a/SampleApps/PingExample/PingExample/LoggerViewModel.swift b/SampleApps/PingExample/PingExample/LoggerViewModel.swift new file mode 100644 index 0000000..91c749c --- /dev/null +++ b/SampleApps/PingExample/PingExample/LoggerViewModel.swift @@ -0,0 +1,98 @@ +// +// LoggerViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import PingLogger + +class LoggerViewModel { + + func setupLogger() { + // shared logger - by default is "none" + var sharedLogger = LogManager.logger + sharedLogger.d("sharedLogger Debug") + sharedLogger.i("sharedLogger Info") + sharedLogger.w("sharedLogger Warning", error: TestError.success) + sharedLogger.e("sharedLogger Error", error: TestError.failure) + + // standard logger - messages of all levels should be displayed in the console... + let standardLogger = LogManager.standard + standardLogger.d("standardLogger Debug") + standardLogger.i("standardLogger Info") + standardLogger.w("standardLogger Warning", error: TestError.success) + standardLogger.e("standardLogger Error", error: TestError.failure) + + // warning logger - only warning and error messages should be displayed... + let warningLogger = LogManager.warning + warningLogger.d("warningLogger Debug") + warningLogger.i("warningLogger Info") + warningLogger.w("warningLogger Warning", error: TestError.success) + warningLogger.e("warningLogger Error", error: TestError.failure) + + // none logger - none of these messages will be displayed... + let noneLogger = LogManager.none + noneLogger.d("noneLogger Debug") + noneLogger.i("noneLogger Info") + noneLogger.w("noneLogger Warning", error: TestError.success) + noneLogger.e("noneLogger Error", error: TestError.failure) + + // switch the shared logger to "standard" - the message from below will be displayed in the console + sharedLogger = LogManager.standard + sharedLogger.d("sharedLogger Debug") + sharedLogger.i("sharedLogger Info") + sharedLogger.w("sharedLogger Warning", error: TestError.success) + sharedLogger.e("sharedLogger Error", error: TestError.failure) + + // test a custom logger + let customLogger = LogManager.customLogger + customLogger.d("customLogger Debug") + customLogger.i("customLogger Info") + customLogger.w("customLogger Warning", error: TestError.success) + customLogger.e("customLogger Error", error: TestError.failure) + } + + enum TestError: Error { + case success + case failure + } +} + + +struct CustomLogger: Logger { + + func i(_ message: String) { + print("\(message) (CustomLogger)") + } + + func d(_ message: String) { + print("\(message) (CustomLogger)") + } + + func w(_ message: String, error: Error?) { + if let error = error { + print("\(message): \(error) (CustomLogger)") + } else { + print("\(message) (CustomLogger)") + } + } + + func e(_ message: String, error: Error?) { + if let error = error { + print("\(message): \(error) (CustomLogger)") + } else { + print("\(message) (CustomLogger)") + } + } +} + +extension LogManager { + static var customLogger: Logger { + return CustomLogger() + } +} diff --git a/SampleApps/PingExample/PingExample/LoginUtility.swift b/SampleApps/PingExample/PingExample/LoginUtility.swift new file mode 100644 index 0000000..68eb2d8 --- /dev/null +++ b/SampleApps/PingExample/PingExample/LoginUtility.swift @@ -0,0 +1,86 @@ +// +// LoginView.swift +// Davinci +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import SwiftUI + +struct LogoView: View { + var body: some View { + Image(systemName: "person.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .foregroundColor(.blue) + } +} + +struct UsernameTextField: View { + + @Binding var username: String + + var body: some View { + TextField("Username", text: $username) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) // Set autocapitalization to none + .padding() + } +} + +struct PasswordTextField: View { + @Binding var password: String + + var body: some View { + SecureField("Password", text: $password) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) // Set autocapitalization to none + .padding() + } +} + +struct LoginButton: View { + var action: () -> Void + var body: some View { + Button(action: action) { + Text("Login") + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } +} + +struct HavingTrouble: View { + var action: () -> Void + var body: some View { + Button(action: action) { + Text("Having Trouble Loggin In") + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } +} + +struct RegisterNow: View { + var action: () -> Void + var body: some View { + Button(action: action) { + Text("Register now") + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } +} + + diff --git a/SampleApps/PingExample/PingExample/LoginView.swift b/SampleApps/PingExample/PingExample/LoginView.swift new file mode 100644 index 0000000..2eb83c5 --- /dev/null +++ b/SampleApps/PingExample/PingExample/LoginView.swift @@ -0,0 +1,173 @@ +// +// LoginView.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import SwiftUI +import PingOrchestrate +import PingDavinci + +struct DavinciView: View { + + @StateObject private var viewmodel = DavinciViewModel() + @Binding var path: [String] + + var body: some View { + ZStack { + ScrollView { + VStack { + Spacer() + switch viewmodel.data.currentNode { + case let nextNode as ContinueNode: + ConnectorView(viewmodel: viewmodel, nextNode: nextNode) + case is SuccessNode: + VStack{}.onAppear { + path.removeLast() + path.append("Token") + } + case let failureNode as FailureNode: + if let nextNode = viewmodel.data.previousNode as? ContinueNode { + ConnectorView(viewmodel: viewmodel, nextNode: nextNode) + } + + let apiError = failureNode.cause as? ApiError + switch apiError { + case .error(_, _, let message): + ErrorView(name: message) + default: + ErrorView(name: "unknown error") + } + + case let errorNode as ErrorNode: + if let nextNode = viewmodel.data.previousNode as? ContinueNode { + ConnectorView(viewmodel: viewmodel, nextNode: nextNode) + } + ErrorView(name: "\(errorNode.message) :: \((errorNode.input["details"] as! [[String: Any]]).first!["message"])") + default: + EmptyView() + } + } + } + + Spacer() + + // Activity indicator + if viewmodel.isLoading { + Color.black.opacity(0.4) + .edgesIgnoringSafeArea(.all) + + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(4) // Increase spinner size if needed + .foregroundColor(.white) // Set spinner color + } + } + } +} + +struct ConnectorView: View { + + @ObservedObject var viewmodel: DavinciViewModel + public var nextNode: ContinueNode + + var body: some View { + VStack { + Image("Logo").resizable().scaledToFill().frame(width: 100, height: 100) + .padding(.vertical, 32) + HeaderView(name: nextNode.name) + DescriptionView(name: nextNode.description) + NewLoginView( + davinciViewModel: viewmodel, + nextNode: nextNode, collectorsList: nextNode.collectors) + } + } +} + +struct ErrorView: View { + var name: String = "" + + var body: some View { + VStack { + Text("\(name)") + .foregroundColor(.red).padding(.top, 20) + } + } +} + +struct HeaderView: View { + var name: String = "" + var body: some View { + VStack { + Text(name) + .font(.title) + } + } +} + +struct DescriptionView: View { + var name: String = "" + var body: some View { + VStack { + Text(name) + .font(.subheadline) + } + } +} + +struct NewLoginView: View { + // MARK: - Propertiers + @ObservedObject var davinciViewModel: DavinciViewModel + + public var nextNode: ContinueNode + + public var collectorsList: Collectors + + // MARK: - View + var body: some View { + + VStack { + + ForEach(collectorsList, id: \.id) { field in + + VStack { + if let text = field as? TextCollector { + InputView(text: text.value, placeholderString: text.label, field: text) + } + + if let password = field as? PasswordCollector { + InputView(placeholderString: password.label, secureField: true, field: password) + } + + if let submitButton = field as? SubmitCollector { + InputButton(title: submitButton.label, field: submitButton) { + Task { + await davinciViewModel.next(node: nextNode) + } + } + } + + }.padding(.horizontal, 5).padding(.top, 20) + + + if let flowButton = field as? FlowCollector { + Button(action: { + flowButton.value = "action" + Task { + await davinciViewModel.next(node: nextNode) + } + }) { + Text(flowButton.label) + .foregroundColor(.black) + } + } + } + } + } +} + diff --git a/SampleApps/PingExample/PingExample/LoginViewModel.swift b/SampleApps/PingExample/PingExample/LoginViewModel.swift new file mode 100644 index 0000000..04e9ce8 --- /dev/null +++ b/SampleApps/PingExample/PingExample/LoginViewModel.swift @@ -0,0 +1,42 @@ +// +// DavinciViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import SwiftUI +import Observation +import PingDavinci +import PingOidc +import PingOrchestrate + +class LoginViewModel: ObservableObject { + + @Published public var isLoading: Bool = false + + @ObservedObject var davinciViewModel: DavinciViewModel + + + init( + isLoading: Bool = false, + davinciViewModel: DavinciViewModel) { + + self.isLoading = isLoading + self.davinciViewModel = davinciViewModel + } + + public func next() async { + isLoading = true + if let nextNode = davinciViewModel.data.currentNode as? ContinueNode { + await davinciViewModel.next(node: nextNode) + isLoading = false + } + } + +} + diff --git a/SampleApps/PingExample/PingExample/Preview Content/Preview Assets.xcassets/Contents.json b/SampleApps/PingExample/PingExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampleApps/PingExample/PingExample/StorageViewModel.swift b/SampleApps/PingExample/PingExample/StorageViewModel.swift new file mode 100644 index 0000000..dddcf97 --- /dev/null +++ b/SampleApps/PingExample/PingExample/StorageViewModel.swift @@ -0,0 +1,40 @@ +// +// StorageViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import PingStorage +import PingLogger + +class StorageViewModel { + + func setupMemoryStorage() async { + do { + let memoryStorage1 = MemoryStorage() + try await memoryStorage1.save(item: "Andy") + let storedValue1 = try await memoryStorage1.get() + LogManager.standard.i("Memory Storage value: \(storedValue1!)") + } catch { + LogManager.standard.e("", error: error) + } + + } + + + func setupKeychainStorage() async { + do { + let keychainStorage = KeychainStorage(account: "token", encryptor: SecuredKeyEncryptor() ?? NoEncryptor()) + try await keychainStorage.save(item: "Jey") + let storedValue = try await keychainStorage.get() + LogManager.standard.i("Kechain Storage value: \(storedValue!)") + } catch { + LogManager.standard.e("", error: error) + } + } +} diff --git a/SampleApps/PingExample/PingExample/TokenViewModel.swift b/SampleApps/PingExample/PingExample/TokenViewModel.swift new file mode 100644 index 0000000..8bf970d --- /dev/null +++ b/SampleApps/PingExample/PingExample/TokenViewModel.swift @@ -0,0 +1,87 @@ +// +// TokenViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import PingLogger + +class TokenViewModel: ObservableObject { + + @Published var accessToken: String = "" + + + init() { + Task { + await accessToken() + } + } + + func accessToken() async { + let token = await davinci.user()?.token() + switch token { + case .success(let accessToken): + await MainActor.run { + self.accessToken = String(describing: accessToken) + } + LogManager.standard.i("AccessToken: \(self.accessToken)") + case .failure(let error): + await MainActor.run { + self.accessToken = "Error: \(error.localizedDescription)" + } + LogManager.standard.e("", error: error) + case .none: + break + } + + } +} + +class UserInfoViewModel: ObservableObject { + + @Published var userInfo: String = "" + + init() { + Task { + await fetchUserInfo() + } + } + + func fetchUserInfo() async { + let userInfo = await davinci.user()?.userinfo(cache: false) + switch userInfo { + case .success(let userInfoDictionary): + await MainActor.run { + var userInfoDescription = "" + userInfoDictionary.forEach { userInfoDescription += "\($0): \($1)\n" } + self.userInfo = userInfoDescription + } + LogManager.standard.i("UserInfo: \(String(describing: self.userInfo))") + case .failure(let error): + await MainActor.run { + self.userInfo = "Error: \(error.localizedDescription)" + } + LogManager.standard.e("", error: error) + case .none: + break + } + } +} + +class LogOutViewModel: ObservableObject { + + @Published var logout: String = "" + + func logout() async { + await davinci.user()?.logout() + await MainActor.run { + logout = "Logout completed" + } + + } +} diff --git a/SampleApps/PingExample/PingExample/UserProfileViewModel.swift b/SampleApps/PingExample/PingExample/UserProfileViewModel.swift new file mode 100644 index 0000000..e0b0046 --- /dev/null +++ b/SampleApps/PingExample/PingExample/UserProfileViewModel.swift @@ -0,0 +1,28 @@ +// +// UserProfileViewModel.swift +// PingExample +// +// Created by jey periyasamy on 5/8/24. +// + +import Foundation + +class UserProfileViewModel: ObservableObject { + + @Published var userProfile: String = "" + + init() { + Task { + await self.accessToken() + } + } + + func accessToken() async { +// let foo = await davinci.user()?.userinfo(cache: <#T##Bool#>) +// await MainActor.run { +// accessToken = foo.debugDescription +// } +// +// print("AccessToken ----->\(String(describing: foo))") + } +} diff --git a/SampleApps/PingExample/PingExampleTests/PingExampleTests.swift b/SampleApps/PingExample/PingExampleTests/PingExampleTests.swift new file mode 100644 index 0000000..4cd132a --- /dev/null +++ b/SampleApps/PingExample/PingExampleTests/PingExampleTests.swift @@ -0,0 +1,39 @@ +// +// PingExampleTests.swift +// PingExampleTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import XCTest +@testable import PingExample + +final class PingExampleTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/SampleApps/PingExample/PingExampleUITests/PingExampleUITests.swift b/SampleApps/PingExample/PingExampleUITests/PingExampleUITests.swift new file mode 100644 index 0000000..022dcf3 --- /dev/null +++ b/SampleApps/PingExample/PingExampleUITests/PingExampleUITests.swift @@ -0,0 +1,44 @@ +// +// PingExampleUITests.swift +// PingExampleUITests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import XCTest + +final class PingExampleUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/SampleApps/PingExample/PingExampleUITests/PingExampleUITestsLaunchTests.swift b/SampleApps/PingExample/PingExampleUITests/PingExampleUITestsLaunchTests.swift new file mode 100644 index 0000000..209ada0 --- /dev/null +++ b/SampleApps/PingExample/PingExampleUITests/PingExampleUITestsLaunchTests.swift @@ -0,0 +1,35 @@ +// +// PingExampleUITestsLaunchTests.swift +// PingExampleUITests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import XCTest + +final class PingExampleUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Storage/README.md b/Storage/README.md new file mode 100644 index 0000000..9a57dc6 --- /dev/null +++ b/Storage/README.md @@ -0,0 +1,105 @@ +

+ + Logo + +


+

+ +# PingStorage SDK + +The PingStorage SDK provides a flexible storage interface and a set of common storage solutions for the Ping SDKs. + +## Integrating the SDK into your project + +Use Cocoapods or Swift Package Manger + +## How to Use the SDK + +### Creating and Using a Storage Instance + +To create a storage instance and use it to persist and retrieve data, follow the example below: + +```swift +// Define the data type that you want to persist +struct Dog: Codable { + let name: String + let type: String +} + + let storage = KeychainStorage(account: "myId") // Create the storage + try? await storage.save(item: Dog(name: "Lucky", type: "Golden Retriever")) // Persist the item + let storedData = try? await storage.get() // Retrieve the item +``` + +Keychain is a storage solution that +uses iOS Keychain to store data securely. + +### Enabling Cache for the Storage + +You can enable cache for the storage as follows, by default cache is disabled: + +```swift + let storage = KeychainStorage(account: "myId", cacheable: true) // Create the Storage with cache enabled +``` + +### Adding Encryption to the Storage + +You can add encryption by specifying the encryptor (`Encryptor` instance) as follows, by default `NoEncryptor` is used: + +```swift + let storage = KeychainStorage(account: "myId", encryptor: SecuredKeyEncryptor() ?? NoEncryptor(), cacheable: true) // Create the Storage with `SecuredKeyEncryptor` +``` + +You can create your custom encryptor by implementing the `Encryptor` protocol: + +```swift +struct MyEncryptor: Encryptor { + func encrypt(data: Data) async throws -> Data { + // Implement the encryption logic + } + + func decrypt(data: Data) async throws -> Data { + // Implement the decryption logic + } +} +``` + +### Creating a Custom Storage + +You can create a custom storage by implementing the `Storage` interface. This could be useful for creating +file-based storage, cloud storage, etc. Here is an example of creating a custom memory storage: + +```swift +public class CustomStorage: Storage { + private var data: T? + + public func save(item: T) async throws { + data = item + } + + public func get() async throws -> T? { + return data + } + + public func delete() async throws { + data = nil + } + +} + +public class CustomStorageDelegate: StorageDelegate { + public init(cacheable: Bool = false) { + super.init(delegate: CustomStorage(), cacheable: cacheable) + } +} + +``` + +## Available Storage Solutions + +The PingStorage SDK provides the following storage solutions: + +| Storage | Description | +|------------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| KeychainStorage | Storage that stores data in iOS Keychain. | +| MemoryStorage | Storage that stores data in memory. | diff --git a/Storage/Storage.xcodeproj/project.pbxproj b/Storage/Storage.xcodeproj/project.pbxproj new file mode 100644 index 0000000..82c681f --- /dev/null +++ b/Storage/Storage.xcodeproj/project.pbxproj @@ -0,0 +1,538 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 95E10E5D2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E10E5C2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift */; }; + A509667B2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A509667A2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift */; }; + A509667D2C45C4E000A4E5B5 /* CustomEncryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A509667C2C45C4E000A4E5B5 /* CustomEncryptorTests.swift */; }; + A54BF44C2BED188000CD24D4 /* SecuredKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF44B2BED188000CD24D4 /* SecuredKey.swift */; }; + A54BF49F2BF2F04E00CD24D4 /* SecuredKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF49E2BF2F04E00CD24D4 /* SecuredKeyTests.swift */; }; + A54BF4E52BF39DC200CD24D4 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF4E42BF39DC200CD24D4 /* MemoryStorageTests.swift */; }; + A54BF4E72BF39E0900CD24D4 /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF4E62BF39E0900CD24D4 /* KeychainStorageTests.swift */; }; + A54BF4E92BF39F8000CD24D4 /* StorageDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF4E82BF39F8000CD24D4 /* StorageDelegateTests.swift */; }; + A57613952C45ABA200EC0E3F /* Encryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57613942C45ABA200EC0E3F /* Encryptor.swift */; }; + A57613972C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57613962C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift */; }; + A588CFA72BF654FA00F9052A /* CustomStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A588CFA62BF654FA00F9052A /* CustomStorageTests.swift */; }; + A5A712422CAC51D800B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A712412CAC51D700B7DD58 /* PrivacyInfo.xcprivacy */; }; + A5A797052BE1782F004D0F2D /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5A796FA2BE1782E004D0F2D /* PingStorage.framework */; }; + A5A7970B2BE1782F004D0F2D /* Storage.h in Headers */ = {isa = PBXBuildFile; fileRef = A5A796FD2BE1782E004D0F2D /* Storage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A5A7971E2BE17C95004D0F2D /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A7971D2BE17C95004D0F2D /* Storage.swift */; }; + A5A797392BE18BE3004D0F2D /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A797382BE18BE3004D0F2D /* MemoryStorage.swift */; }; + A5A7973B2BE2A035004D0F2D /* StorageDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A7973A2BE2A035004D0F2D /* StorageDelegate.swift */; }; + A5A7974D2BE33279004D0F2D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A7974C2BE33279004D0F2D /* KeychainStorage.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + A5A797062BE1782F004D0F2D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A5A796F12BE1782E004D0F2D /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5A796F92BE1782E004D0F2D; + remoteInfo = PingStorage; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 95E10E5C2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedKeychainStorageTests.swift; sourceTree = ""; }; + A509667A2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuredKeyEncryptorTests.swift; sourceTree = ""; }; + A509667C2C45C4E000A4E5B5 /* CustomEncryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEncryptorTests.swift; sourceTree = ""; }; + A54BF44B2BED188000CD24D4 /* SecuredKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuredKey.swift; sourceTree = ""; }; + A54BF49E2BF2F04E00CD24D4 /* SecuredKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuredKeyTests.swift; sourceTree = ""; }; + A54BF4E42BF39DC200CD24D4 /* MemoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorageTests.swift; sourceTree = ""; }; + A54BF4E62BF39E0900CD24D4 /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = ""; }; + A54BF4E82BF39F8000CD24D4 /* StorageDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageDelegateTests.swift; sourceTree = ""; }; + A57613942C45ABA200EC0E3F /* Encryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encryptor.swift; sourceTree = ""; }; + A57613962C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuredKeyEncryptor.swift; sourceTree = ""; }; + A588CFA62BF654FA00F9052A /* CustomStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStorageTests.swift; sourceTree = ""; }; + A5A712412CAC51D700B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + A5A796FA2BE1782E004D0F2D /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A5A796FD2BE1782E004D0F2D /* Storage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Storage.h; sourceTree = ""; }; + A5A797042BE1782F004D0F2D /* StorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A5A7971D2BE17C95004D0F2D /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + A5A797382BE18BE3004D0F2D /* MemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorage.swift; sourceTree = ""; }; + A5A7973A2BE2A035004D0F2D /* StorageDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageDelegate.swift; sourceTree = ""; }; + A5A7974C2BE33279004D0F2D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A5A796F72BE1782E004D0F2D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A5A797012BE1782F004D0F2D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A797052BE1782F004D0F2D /* PingStorage.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A5A796F02BE1782E004D0F2D = { + isa = PBXGroup; + children = ( + A5A796FC2BE1782E004D0F2D /* Storage */, + A5A797082BE1782F004D0F2D /* StorageTests */, + A5A796FB2BE1782E004D0F2D /* Products */, + ); + sourceTree = ""; + }; + A5A796FB2BE1782E004D0F2D /* Products */ = { + isa = PBXGroup; + children = ( + A5A796FA2BE1782E004D0F2D /* PingStorage.framework */, + A5A797042BE1782F004D0F2D /* StorageTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + A5A796FC2BE1782E004D0F2D /* Storage */ = { + isa = PBXGroup; + children = ( + A5A712412CAC51D700B7DD58 /* PrivacyInfo.xcprivacy */, + A5A796FD2BE1782E004D0F2D /* Storage.h */, + A57613942C45ABA200EC0E3F /* Encryptor.swift */, + A5A7974C2BE33279004D0F2D /* KeychainStorage.swift */, + A5A797382BE18BE3004D0F2D /* MemoryStorage.swift */, + A54BF44B2BED188000CD24D4 /* SecuredKey.swift */, + A57613962C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift */, + A5A7971D2BE17C95004D0F2D /* Storage.swift */, + A5A7973A2BE2A035004D0F2D /* StorageDelegate.swift */, + ); + path = Storage; + sourceTree = ""; + }; + A5A797082BE1782F004D0F2D /* StorageTests */ = { + isa = PBXGroup; + children = ( + A509667C2C45C4E000A4E5B5 /* CustomEncryptorTests.swift */, + A588CFA62BF654FA00F9052A /* CustomStorageTests.swift */, + A54BF4E62BF39E0900CD24D4 /* KeychainStorageTests.swift */, + 95E10E5C2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift */, + A54BF4E42BF39DC200CD24D4 /* MemoryStorageTests.swift */, + A54BF49E2BF2F04E00CD24D4 /* SecuredKeyTests.swift */, + A509667A2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift */, + A54BF4E82BF39F8000CD24D4 /* StorageDelegateTests.swift */, + ); + path = StorageTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A5A796F52BE1782E004D0F2D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A7970B2BE1782F004D0F2D /* Storage.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A5A796F92BE1782E004D0F2D /* PingStorage */ = { + isa = PBXNativeTarget; + buildConfigurationList = A5A7970E2BE1782F004D0F2D /* Build configuration list for PBXNativeTarget "PingStorage" */; + buildPhases = ( + A5A796F52BE1782E004D0F2D /* Headers */, + A5A796F62BE1782E004D0F2D /* Sources */, + A5A796F72BE1782E004D0F2D /* Frameworks */, + A5A796F82BE1782E004D0F2D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PingStorage; + productName = PingStorage; + productReference = A5A796FA2BE1782E004D0F2D /* PingStorage.framework */; + productType = "com.apple.product-type.framework"; + }; + A5A797032BE1782F004D0F2D /* StorageTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A5A797112BE1782F004D0F2D /* Build configuration list for PBXNativeTarget "StorageTests" */; + buildPhases = ( + A5A797002BE1782F004D0F2D /* Sources */, + A5A797012BE1782F004D0F2D /* Frameworks */, + A5A797022BE1782F004D0F2D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A5A797072BE1782F004D0F2D /* PBXTargetDependency */, + ); + name = StorageTests; + productName = PingStorageTests; + productReference = A5A797042BE1782F004D0F2D /* StorageTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A5A796F12BE1782E004D0F2D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + A5A796F92BE1782E004D0F2D = { + CreatedOnToolsVersion = 15.3; + }; + A5A797032BE1782F004D0F2D = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = A5A796F42BE1782E004D0F2D /* Build configuration list for PBXProject "Storage" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A5A796F02BE1782E004D0F2D; + productRefGroup = A5A796FB2BE1782E004D0F2D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A5A796F92BE1782E004D0F2D /* PingStorage */, + A5A797032BE1782F004D0F2D /* StorageTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A5A796F82BE1782E004D0F2D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A712422CAC51D800B7DD58 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A5A797022BE1782F004D0F2D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A5A796F62BE1782E004D0F2D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5A797392BE18BE3004D0F2D /* MemoryStorage.swift in Sources */, + A57613972C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift in Sources */, + A54BF44C2BED188000CD24D4 /* SecuredKey.swift in Sources */, + A5A7973B2BE2A035004D0F2D /* StorageDelegate.swift in Sources */, + A5A7974D2BE33279004D0F2D /* KeychainStorage.swift in Sources */, + A57613952C45ABA200EC0E3F /* Encryptor.swift in Sources */, + A5A7971E2BE17C95004D0F2D /* Storage.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A5A797002BE1782F004D0F2D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A509667B2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift in Sources */, + A54BF4E92BF39F8000CD24D4 /* StorageDelegateTests.swift in Sources */, + A54BF4E72BF39E0900CD24D4 /* KeychainStorageTests.swift in Sources */, + A509667D2C45C4E000A4E5B5 /* CustomEncryptorTests.swift in Sources */, + A54BF4E52BF39DC200CD24D4 /* MemoryStorageTests.swift in Sources */, + 95E10E5D2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift in Sources */, + A54BF49F2BF2F04E00CD24D4 /* SecuredKeyTests.swift in Sources */, + A588CFA72BF654FA00F9052A /* CustomStorageTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + A5A797072BE1782F004D0F2D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5A796F92BE1782E004D0F2D /* PingStorage */; + targetProxy = A5A797062BE1782F004D0F2D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + A5A7970C2BE1782F004D0F2D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + A5A7970D2BE1782F004D0F2D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + A5A7970F2BE1782F004D0F2D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Storage; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A5A797102BE1782F004D0F2D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 9QSE66762D; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0"; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface"; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Storage; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + A5A797122BE1782F004D0F2D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.StorageTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Debug; + }; + A5A797132BE1782F004D0F2D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9QSE66762D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.StorageTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A5A796F42BE1782E004D0F2D /* Build configuration list for PBXProject "Storage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5A7970C2BE1782F004D0F2D /* Debug */, + A5A7970D2BE1782F004D0F2D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A5A7970E2BE1782F004D0F2D /* Build configuration list for PBXNativeTarget "PingStorage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5A7970F2BE1782F004D0F2D /* Debug */, + A5A797102BE1782F004D0F2D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A5A797112BE1782F004D0F2D /* Build configuration list for PBXNativeTarget "StorageTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5A797122BE1782F004D0F2D /* Debug */, + A5A797132BE1782F004D0F2D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A5A796F12BE1782E004D0F2D /* Project object */; +} diff --git a/Storage/Storage.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme b/Storage/Storage.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme new file mode 100644 index 0000000..f2681d7 --- /dev/null +++ b/Storage/Storage.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Storage/Storage/Encryptor.swift b/Storage/Storage/Encryptor.swift new file mode 100644 index 0000000..92ba741 --- /dev/null +++ b/Storage/Storage/Encryptor.swift @@ -0,0 +1,50 @@ +// +// Encryptor.swift +// PingStorage +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// A protocol that defines methods for encrypting and decrypting data. +public protocol Encryptor { + /// Encrypts the given data. + /// - Parameter data: The data to encrypt. + /// - Returns: The encrypted data. + /// - Throws: An error if encryption fails. + func encrypt(data: Data) async throws -> Data + + /// Decrypts the given data. + /// - Parameter data: The data to decrypt. + /// - Returns: The decrypted data. + /// - Throws: An error if decryption fails. + func decrypt(data: Data) async throws -> Data +} + + +/// A struct that provides no encryption. +public struct NoEncryptor: Encryptor { + /// Initializes a new instance of `NoEncryptor`. + public init() {} + + /// Returns the given data without performing any encryption. + /// + /// - Parameter data: The data to "encrypt". + /// - Returns: The same data that was provided. + public func encrypt(data: Data) async throws -> Data { + return data + } + + /// Returns the given data without performing any decryption. + /// + /// - Parameter data: The data to "decrypt". + /// - Returns: The same data that was provided. + public func decrypt(data: Data) async throws -> Data { + return data + } +} diff --git a/Storage/Storage/KeychainStorage.swift b/Storage/Storage/KeychainStorage.swift new file mode 100644 index 0000000..4cca3b4 --- /dev/null +++ b/Storage/Storage/KeychainStorage.swift @@ -0,0 +1,125 @@ +// +// KeychainStorage.swift +// PingStorage +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// A storage for storing `Codable` objects in the Keychain +public class Keychain: Storage { + private var account: String + private let service: String = "com.pingidentity.keychainService" + private let encryptor: Encryptor + + /// Initializer for Keychain + /// - Parameters: + /// - account: String indicating the item's account(key) name. + /// - encryptor: Encryptor for encrypting stored data. Default value is `NoEncryptor()` + public init(account: String, encryptor: Encryptor = NoEncryptor()) { + self.account = account + self.encryptor = encryptor + } + + /// Saves the given item in the keychain. + /// - Parameter item: The item to save. + public func save(item: T) async throws { + let data = try JSONEncoder().encode(item) + var query = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: account, + kSecAttrService as String: service, + kSecValueData as String: data + ] as [String: Any] + + query[kSecValueData as String] = try await encryptor.encrypt(data: data) + + SecItemDelete(query as CFDictionary) // Remove any existing item + let status = SecItemAdd(query as CFDictionary, nil) + + guard status == errSecSuccess else { + throw KeychainError.unableToSave + } + } + + /// Retrieves the item from the keychain. + /// - Returns: The item if it exists, `nil` otherwise. + public func get() async throws -> T? { + let query = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: account, + kSecAttrService as String: service, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnData as String: kCFBooleanTrue! + ] as [String: Any] + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + guard status == errSecSuccess, let data = item as? Data else { + return nil + } + + return try JSONDecoder().decode(T.self, from: try await encryptor.decrypt(data: data)) + } + + /// Deletes the item from memory. + public func delete() async throws { + let query = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: account, + kSecAttrService as String: service + ] as [String: Any] + + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess else { + throw KeychainError.unableToDelete + } + } +} + + +/// `KeychainError` represents errors that can occur while interacting with the keychain. +public enum KeychainError: LocalizedError { + case unableToSave + case unableToRetrieve + case unableToDelete + + /// A localized message describing what error occurred. + public var errorMessage: String { + switch self { + case .unableToSave: + return "Uanble to save to the keychain" + case .unableToRetrieve: + return "Uanble to retrieve from the keychain" + case .unableToDelete: + return "Unable to delete from the kechain" + } + } +} + + +/// `KeychainStorage` is a generic class that conforms to the `StorageDelegate` protocol, providing a secure storage solution by leveraging the keychain. +/// It is designed to store, retrieve, and manage objects of type `T`, where `T` must conform to the `Codable` protocol. This requirement ensures that the objects can be easily encoded and decoded for secure storage in the keychain. +/// +/// - Parameter T: The type of the objects to be stored in the keychain. Must conform to `Codable`. +public class KeychainStorage: StorageDelegate { + /// Initializes a new instance of `KeychainStorage`. + /// + /// This initializer configures a `KeychainStorage` instance with a specified account and security settings. + /// It allows storing data securely in the keychain using the provided account identifier. T + /// + /// - Parameters: + /// - account: A `String` identifying the keychain account under which the data will be stored. This is used + /// to differentiate between different sets of data within the keychain. + /// - encryptor: An `Encryptor` instance for encrypting/decrypting the stored data. Default value is `NoEncryptor()` + /// - cacheable: A `Bool` indicating whether the stored data should be cached. Defaults to `false`. + public init(account: String, encryptor: Encryptor = NoEncryptor(), cacheable: Bool = false) { + super.init(delegate: Keychain(account: account, encryptor: encryptor), cacheable: cacheable) + } +} diff --git a/Storage/Storage/MemoryStorage.swift b/Storage/Storage/MemoryStorage.swift new file mode 100644 index 0000000..7faef19 --- /dev/null +++ b/Storage/Storage/MemoryStorage.swift @@ -0,0 +1,56 @@ +// +// MemoryStorage.swift +// PingStorage +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// A storage for storing objects in memory, where `T` is the type of the object to be stored. +public class Memory: Storage { + private var data: T? + + /// Saves the given item in memory. + /// - Parameter item: The item to save. + public func save(item: T) async throws { + data = item + } + + /// Retrieves the item from memory. + /// - Returns: The item if it exists, `nil` otherwise. + public func get() async throws -> T? { + return data + } + + /// Deletes the item from memory. + public func delete() async throws { + data = nil + } +} + + +/// `MemoryStorage` provides an in-memory storage solution for objects of type `T`. +/// It conforms to the `StorageDelegate` protocol, enabling it to interact seamlessly with other components expecting a storage delegate. +/// This class is ideal for temporary storage where persistence across app launches is not required. +/// +/// The generic type `T` must conform to `Codable` to ensure that objects can be encoded and decoded when written to and read from memory, respectively. +/// +/// - Parameter T: The type of the objects to be stored. Must conform to `Codable`. +public class MemoryStorage: StorageDelegate { + /// Initializes a new instance of `MemoryStorage`. + /// + /// This initializer creates a `MemoryStorage` instance that acts as a delegate for an in-memory storage + /// mechanism. It allows for the optional caching of data based on the `cacheable` parameter. + /// + /// - Parameter cacheable: A Boolean value indicating whether the stored data should be cached. Defaults to `false`, + /// which means that caching is not enabled by default. When set to `true`, it enables caching + /// based on the implementation details of the `Memory` storage strategy. + public init(cacheable: Bool = false) { + super.init(delegate: Memory(), cacheable: cacheable) + } +} diff --git a/Storage/Storage/PrivacyInfo.xcprivacy b/Storage/Storage/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..fcfc9b9 --- /dev/null +++ b/Storage/Storage/PrivacyInfo.xcprivacy @@ -0,0 +1,10 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/Storage/Storage/SecuredKey.swift b/Storage/Storage/SecuredKey.swift new file mode 100644 index 0000000..464832a --- /dev/null +++ b/Storage/Storage/SecuredKey.swift @@ -0,0 +1,164 @@ +// +// SecuredKey.swift +// PingStorage +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import LocalAuthentication +import CryptoKit + +/// `SecuredKey` is a representation of Secure Enclave keypair and performing PKI using Secure Enclave +public struct SecuredKey { + /// Private Key of SecuredKey + fileprivate var privateKey: SecKey + /// Public Key of SecuredKey + fileprivate var publicKey: SecKey + /// Algorithm to be used for encryption/decryption using SecuredKey + fileprivate let oldAlgorithm: SecKeyAlgorithm = .eciesEncryptionCofactorX963SHA256AESGCM + + /// Validates whether SecuredKey using Secure Enclave is available on the device or not + public static func isAvailable() -> Bool { + return SecureEnclave.isAvailable + } + + /// Initializes `SecuredKey` object with designated service; `SecuredKey` may return `nil` if it failed to generate keypair + /// - Parameter applicationTag: Unique identifier for SecuredKey + public init?(applicationTag: String) { + guard SecuredKey.isAvailable() else { + return nil + } + + // If SecuredKey already exists, return from the storage + if let privateKey = SecuredKey.readKey(applicationTag: applicationTag) { + self.privateKey = privateKey + } + else { + // Otherwise, generate new keypair + do { + self.privateKey = try SecuredKey.generateKey(applicationTag: applicationTag, accessGroup: nil, accessibility: kSecAttrAccessibleAfterFirstUnlock) + } + catch { + return nil + } + } + + // Copy the public key from the private key + if let publicKey = SecKeyCopyPublicKey(self.privateKey) { + self.publicKey = publicKey + } + else { + return nil + } + } + + /// Retrieves private key with given 'ApplicationTag' + /// - Parameter applicationTag: Application Tag string value for private key + static func readKey(applicationTag: String, accessGroup: String? = nil) -> SecKey? { + var query = [String: Any]() + query[String(kSecClass)] = kSecClassKey + query[String(kSecAttrKeyType)] = String(kSecAttrKeyTypeEC) + query[String(kSecReturnRef)] = true + query[String(kSecAttrApplicationTag)] = applicationTag + + if let accessGroup = accessGroup { + query[String(kSecAttrAccessGroup)] = accessGroup + } + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + guard status == errSecSuccess else { + return nil + } + return (item as! SecKey) + } + + /// Generates private key with given 'ApplicationTag' + /// - Parameter applicationTag: Application Tag string value for private key + static func generateKey(applicationTag: String, accessGroup: String? = nil, accessibility: CFString) throws -> SecKey { + var query = [String: Any]() + + query[String(kSecAttrKeyType)] = String(kSecAttrKeyTypeEC) + query[String(kSecAttrKeySizeInBits)] = 256 + + if let accessGroup = accessGroup { + query[String(kSecAttrAccessGroup)] = accessGroup + } + + var keyAttr = [String: Any]() + keyAttr[String(kSecAttrIsPermanent)] = true + keyAttr[String(kSecAttrApplicationTag)] = applicationTag + +#if !targetEnvironment(simulator) + // If the device supports Secure Enclave, create a keypair using Secure Enclave TokenID + if SecuredKey.isAvailable() { + query[String(kSecAttrTokenID)] = String(kSecAttrTokenIDSecureEnclave) + let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, .privateKeyUsage, nil)! + keyAttr[String(kSecAttrAccessControl)] = accessControl + } +#endif + + query[String(kSecPrivateKeyAttrs)] = keyAttr + + var error: Unmanaged? + guard let privateKey = SecKeyCreateRandomKey(query as CFDictionary, &error) else { + throw error!.takeRetainedValue() as Error + } + + return privateKey + } + + /// Deletes private key with given 'Application Tag' + /// - Parameter applicationTag: Application Tag string value for private key + static func deleteKey(applicationTag: String) { + var query = [String: Any]() + query[String(kSecClass)] = String(kSecClassKey) + query[String(kSecAttrApplicationTag)] = applicationTag + SecItemDelete(query as CFDictionary) + } + + /// Encrypts Data object using `SecuredKey` object + /// - Parameter data: Encrypted Data object + /// - Parameter secAlgorithm: Algorithm to be used for encryption. Default: `.eciesEncryptionCofactorVariableIVX963SHA256AESGCM` + /// - Returns: Encrypted Data object or `nil` if encryption fails + public func encrypt(data: Data, secAlgorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM) -> Data? { + + guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, secAlgorithm) else { + return nil + } + + var error: Unmanaged? + let encryptedData = SecKeyCreateEncryptedData(publicKey, secAlgorithm, data as CFData, &error) as Data? + return encryptedData + } + + /// Decrypts Data object using SecuredKey object + /// - Parameter data: Decrypted Data object + /// - Parameter secAlgorithm: Algorithm to be used for decryption. Default: `.eciesEncryptionCofactorVariableIVX963SHA256AESGCM` + /// - Returns: Decrypted Data object or `nil` if decryption fails + public func decrypt(data: Data, secAlgorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM) -> Data? { + + guard SecKeyIsAlgorithmSupported(privateKey, .decrypt, secAlgorithm) else { + return nil + } + + var error: Unmanaged? + let decryptedData = SecKeyCreateDecryptedData(privateKey, secAlgorithm, data as CFData, &error) as Data? + if error != nil { + var decryptError: Unmanaged? + let decryptedData = SecKeyCreateDecryptedData(privateKey, oldAlgorithm, data as CFData, &decryptError) as Data? + if decryptError != nil { + return nil + } else { + return decryptedData + } + + } + return decryptedData + } +} diff --git a/Storage/Storage/SecuredKeyEncryptor.swift b/Storage/Storage/SecuredKeyEncryptor.swift new file mode 100644 index 0000000..800e5a0 --- /dev/null +++ b/Storage/Storage/SecuredKeyEncryptor.swift @@ -0,0 +1,68 @@ +// +// SecuredKeyEncryptor.swift +// PingStorage +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// A struct that provides encryption and decryption functionalities using `SecuredKey`. +public struct SecuredKeyEncryptor: Encryptor { + private let securedKeyTag: String = "com.pingidentity.securedKey.identifier" + private var securedKey: SecuredKey + + /// Initializes a new instance of `SecuredKeyEncryptor`. + /// + /// This initializer attempts to create a `SecuredKey` with the given application tag. + /// If it fails, the initializer returns `nil`. + public init?() { + guard let securedKey = SecuredKey(applicationTag: self.securedKeyTag) else { + return nil + } + self.securedKey = securedKey + } + + /// Encrypts the given data. + /// - Parameter data: The data to encrypt. + /// - Returns: The encrypted data. + /// - Throws: `EncryptorError.failedToEncrypt` if the encryption fails. + public func encrypt(data: Data) async throws -> Data { + guard let encryptedData = securedKey.encrypt(data: data) else { + throw EncryptorError.failedToEncrypt + } + return encryptedData + } + + /// Decrypts the given data. + /// - Parameter data: The data to decrypt. + /// - Returns: The decrypted data. + /// - Throws: `EncryptorError.failedToDecrypt` if the decryption fails. + public func decrypt(data: Data) async throws -> Data { + guard let decryptedData = securedKey.decrypt(data: data) else { + throw EncryptorError.failedToDecrypt + } + return decryptedData + } +} + + +/// `EncryptorError` represents errors that can occur while encrypting/decrypting. +public enum EncryptorError: LocalizedError { + case failedToEncrypt + case failedToDecrypt + + /// A localized message describing what error occurred. + public var errorMessage: String { + switch self { + case .failedToEncrypt: + return "Failed to encrypt given data" + case .failedToDecrypt: + return "Failed to decrypt given data" + } + } +} diff --git a/Storage/Storage/Storage.h b/Storage/Storage/Storage.h new file mode 100644 index 0000000..212d12f --- /dev/null +++ b/Storage/Storage/Storage.h @@ -0,0 +1,21 @@ +// +// Storage.h +// Storage +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +#import + +//! Project version number for Storage. +FOUNDATION_EXPORT double StorageVersionNumber; + +//! Project version string for Storage. +FOUNDATION_EXPORT const unsigned char StorageVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Storage/Storage/Storage.swift b/Storage/Storage/Storage.swift new file mode 100644 index 0000000..ee9f1d0 --- /dev/null +++ b/Storage/Storage/Storage.swift @@ -0,0 +1,28 @@ +// +// Storage.swift +// PingStorage +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// Protocol to persist and retrieve `Codable` instanse. +public protocol Storage { + associatedtype T: Codable + + /// Saves the given item. + /// - Parameter item: The item to be saved. + func save(item: T) async throws + + /// Retrieves the stored item. + /// - Returns: The stored item, or null if no item is stored. + func get() async throws -> T? + + /// Deletes the stored item. + func delete() async throws +} diff --git a/Storage/Storage/StorageDelegate.swift b/Storage/Storage/StorageDelegate.swift new file mode 100644 index 0000000..dee56d4 --- /dev/null +++ b/Storage/Storage/StorageDelegate.swift @@ -0,0 +1,94 @@ +// +// StorageDelegate.swift +// PingStorage +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +/// A storage delegate class that delegates its operations to a storage. +/// It can optionally cache the stored item in memory. +/// This class is designed to be subclassed by specific storage strategies (e.g., keychain, in-memory) that conform to the `Storage` protocol. +/// +/// - Parameter T: The type of the object being stored. Must conform to `Codable` to ensure that +/// object can be easily encoded and decoded. +open class StorageDelegate: Storage { + private let delegate: any Storage + private let cacheable: Bool + private var cached: T? + private let queue = DispatchQueue(label: "com.ping.storage.queue") + + /// Initializer for StorageDelegate + /// - Parameters: + /// - delegate: The storage to delegate the operations to. + /// - cacheable: Whether the storage delegate should cache the object in memory. + public init(delegate: any Storage, cacheable: Bool = false) { + self.delegate = delegate + self.cacheable = cacheable + } + + /// Saves the given item in the storage and optionally in memory. + /// - Parameter item: The item to save. + public func save(item: T) async throws { + try await withCheckedThrowingContinuation { continuation in + queue.async { + Task { + do { + try await self.delegate.save(item: item) + if self.cacheable { + self.cached = item + } + continuation.resume() + } catch { + continuation.resume(throwing: error) + } + } + } + } + } + + /// Retrieves the item from memory if it's cached, otherwise from the storage. + /// - Returns: The item if it exists, `nil` otherwise. + public func get() async throws -> T? { + try await withCheckedThrowingContinuation { continuation in + queue.async { + Task { + do { + if let cached = self.cached { + continuation.resume(returning: cached) + } else { + let item = try await self.delegate.get() + continuation.resume(returning: item) + } + } catch { + continuation.resume(throwing: error) + } + } + } + } + } + + /// Deletes the item from the storage and removes it from memory if it's cached. + public func delete() async throws { + try await withCheckedThrowingContinuation { continuation in + queue.async { + Task { + do { + try await self.delegate.delete() + if self.cacheable { + self.cached = nil + } + continuation.resume() + } catch { + continuation.resume(throwing: error) + } + } + } + } + } +} diff --git a/Storage/StorageTests/CustomEncryptorTests.swift b/Storage/StorageTests/CustomEncryptorTests.swift new file mode 100644 index 0000000..8e80e58 --- /dev/null +++ b/Storage/StorageTests/CustomEncryptorTests.swift @@ -0,0 +1,70 @@ +// +// CustomEncryptorTests.swift +// StorageTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import XCTest +@testable import PingStorage + +final class CustomEncryptorTests: XCTestCase { + var encryptor: CustomEncryptor! + + override func setUp() { + super.setUp() + encryptor = CustomEncryptor() + } + + override func tearDown() { + encryptor = nil + super.tearDown() + } + + // TestRailCase(24716) + func testEncryption() async { + let data = Data("Test data".utf8) + + do { + let encryptedData = try await encryptor.encrypt(data: data) + let encryptedString = String(decoding: encryptedData, as: UTF8.self) + XCTAssertTrue(encryptedString.contains("_ENCRYPTED_"), "Encrypted data should contain '_ENCRYPTED_'") + } catch { + XCTFail("Encryption failed with error: \(error)") + } + } + + // TestRailCase(24716) + func testDecryption() async { + let data = Data("Test data".utf8) + let encryptedData = try! await encryptor.encrypt(data: data) + + do { + let decryptedData = try await encryptor.decrypt(data: encryptedData) + let decryptedString = String(decoding: decryptedData, as: UTF8.self) + XCTAssertEqual(decryptedString, "Test data", "Decrypted data should match original data") + } catch { + XCTFail("Decryption failed with error: \(error)") + } + } +} + +struct CustomEncryptor: Encryptor { + + func encrypt(data: Data) async throws -> Data { + let stringData = String(decoding: data, as: UTF8.self) + let encryptedString = stringData + "_ENCRYPTED_" + return Data(encryptedString.utf8) + } + + func decrypt(data: Data) async throws -> Data { + let stringData = String(decoding: data, as: UTF8.self) + let decryptedString = stringData.replacingOccurrences(of: "_ENCRYPTED_", with: "") + return Data(decryptedString.utf8) + } +} diff --git a/Storage/StorageTests/CustomStorageTests.swift b/Storage/StorageTests/CustomStorageTests.swift new file mode 100644 index 0000000..6bf3cc8 --- /dev/null +++ b/Storage/StorageTests/CustomStorageTests.swift @@ -0,0 +1,77 @@ +// +// CustomStorageTests.swift +// StorageTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingStorage + +final class CustomStorageTests: XCTestCase { + + private var customStorage: CustomStorageDelegate! + + override func setUp() { + super.setUp() + customStorage = CustomStorageDelegate() + } + + override func tearDown() { + customStorage = nil + super.tearDown() + } + + // TestRailCase(24710) + func testSaveItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await customStorage.save(item: item) + let retrievedItem = try await customStorage.get() + XCTAssertEqual(retrievedItem, item) + } + + // TestRailCase(24711) + func testGetItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await customStorage.save(item: item) + let retrievedItem = try await customStorage.get() + XCTAssertEqual(retrievedItem, item) + } + + // TestRailCase(24714) + func testDeleteItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await customStorage.save(item: item) + try await customStorage.delete() + let retrievedItem = try await customStorage.get() + XCTAssertNil(retrievedItem) + } + +} + +public class CustomStorage: Storage { + private var data: T? + + public func save(item: T) async throws { + data = item + } + + public func get() async throws -> T? { + return data + } + + public func delete() async throws { + data = nil + } + +} + +public class CustomStorageDelegate: StorageDelegate { + public init(cacheable: Bool = false) { + super.init(delegate: CustomStorage(), cacheable: cacheable) + } +} diff --git a/Storage/StorageTests/EncryptedKeychainStorageTests.swift b/Storage/StorageTests/EncryptedKeychainStorageTests.swift new file mode 100644 index 0000000..6a22101 --- /dev/null +++ b/Storage/StorageTests/EncryptedKeychainStorageTests.swift @@ -0,0 +1,56 @@ +// +// KeychainStorageTests.swift +// StorageTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingStorage + +final class EncryptedKeychainStorageTests: XCTestCase { + private var keychainStorage: KeychainStorage! + + override func setUp() { + super.setUp() + // Test KeychainStorage with the SecuredKeyEncryptor - the OOTB encryptor provided by the SDK + keychainStorage = KeychainStorage(account: "testAccount", encryptor: SecuredKeyEncryptor()!) + } + + override func tearDown() { + Task { + try? await keychainStorage.delete() + keychainStorage = nil + } + super.tearDown() + } + + // TestRailCase(24706) + func testSaveItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await keychainStorage.save(item: item) + let retrievedItem = try await keychainStorage.get() + XCTAssertEqual(retrievedItem, item) + } + + // TestRailCase(24707) + func testGetItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await keychainStorage.save(item: item) + let retrievedItem = try await keychainStorage.get() + XCTAssertEqual(retrievedItem, item) + } + + // TestRailCase(24708) + func testDeleteItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await keychainStorage.save(item: item) + try await keychainStorage.delete() + let retrievedItem = try await keychainStorage.get() + XCTAssertNil(retrievedItem) + } +} diff --git a/Storage/StorageTests/KeychainStorageTests.swift b/Storage/StorageTests/KeychainStorageTests.swift new file mode 100644 index 0000000..867c632 --- /dev/null +++ b/Storage/StorageTests/KeychainStorageTests.swift @@ -0,0 +1,56 @@ +// +// KeychainStorageTests.swift +// StorageTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingStorage + +final class KeychainStorageTests: XCTestCase { + private var keychainStorage: KeychainStorage! + + override func setUp() { + super.setUp() + // By default the KeychainStorage does not use encryption + keychainStorage = KeychainStorage(account: "testAccount") + } + + override func tearDown() { + Task { + try? await keychainStorage.delete() + keychainStorage = nil + } + super.tearDown() + } + + // TestRailCase(24703) + func testSaveItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await keychainStorage.save(item: item) + let retrievedItem = try await keychainStorage.get() + XCTAssertEqual(retrievedItem, item) + } + + // TestRailCase(24704) + func testGetItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await keychainStorage.save(item: item) + let retrievedItem = try await keychainStorage.get() + XCTAssertEqual(retrievedItem, item) + } + + // TestRailCase(24705) + func testDeleteItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await keychainStorage.save(item: item) + try await keychainStorage.delete() + let retrievedItem = try await keychainStorage.get() + XCTAssertNil(retrievedItem) + } +} diff --git a/Storage/StorageTests/MemoryStorageTests.swift b/Storage/StorageTests/MemoryStorageTests.swift new file mode 100644 index 0000000..7558557 --- /dev/null +++ b/Storage/StorageTests/MemoryStorageTests.swift @@ -0,0 +1,80 @@ +// +// MemoryStorageTests.swift +// StorageTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingStorage + +final class MemoryStorageTests: XCTestCase { + private var memoryStorage: MemoryStorage! + private var memoryStorageMulti: MemoryStorage>! + + override func setUp() { + super.setUp() + memoryStorage = MemoryStorage() + memoryStorageMulti = MemoryStorage() + } + + override func tearDown() { + memoryStorage = nil + memoryStorageMulti = nil + super.tearDown() + } + + // TestRailCase(21622) + func testSaveItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await memoryStorage.save(item: item) + let retrievedItem = try await memoryStorage.get() + XCTAssertEqual(retrievedItem, item) + } + + // TestRailCase(21623) + func testGetItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await memoryStorage.save(item: item) + let retrievedItem = try await memoryStorage.get() + XCTAssertEqual(retrievedItem, item) + } + + // TestRailCase(21626) + func testDeleteItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await memoryStorage.save(item: item) + try await memoryStorage.delete() + let retrievedItem = try await memoryStorage.get() + XCTAssertNil(retrievedItem) + } + + // TestRailCase(21624, 21625) + func testMultipleData() async throws { + var itemsArray = [TestItem]() + let item1 = TestItem(id: 1, name: "Test1") + let item2 = TestItem(id: 2, name: "Test2") + + itemsArray.append(item1) + itemsArray.append(item2) + + // Save in memory storage + try await memoryStorageMulti.save(item: itemsArray) + + // Restore from memory storage + let retrievedItem = try await memoryStorageMulti.get() + + XCTAssertEqual(retrievedItem, itemsArray) + XCTAssertEqual(retrievedItem![0], item1) + XCTAssertEqual(retrievedItem![1], item2) + } +} + +struct TestItem: Codable, Equatable { + let id: Int + let name: String +} diff --git a/Storage/StorageTests/SecuredKeyEncryptorTests.swift b/Storage/StorageTests/SecuredKeyEncryptorTests.swift new file mode 100644 index 0000000..3780d39 --- /dev/null +++ b/Storage/StorageTests/SecuredKeyEncryptorTests.swift @@ -0,0 +1,59 @@ +// +// SecuredKeyEncryptorTests.swift +// StorageTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingStorage + +final class SecuredKeyEncryptorTests: XCTestCase { + + var encryptor: SecuredKeyEncryptor? + + override func setUp() { + super.setUp() + encryptor = SecuredKeyEncryptor() + } + + override func tearDown() { + encryptor = nil + super.tearDown() + } + + func testInitialization() { + XCTAssertNotNil(encryptor, "Initialization should succeed") + } + + // TestRailCase(24709) + func testEncryption() async { + let data = Data("Test data".utf8) + + do { + let encryptedData = try await encryptor?.encrypt(data: data) + XCTAssertNotNil(encryptedData, "Encryption should succeed") + } catch { + XCTFail("Encryption failed with error: \(error)") + } + } + + // TestRailCase(24709) + func testDecryption() async { + let data = Data("Test data".utf8) + + do { + let encryptedData = try await encryptor?.encrypt(data: data) + let decryptedData = try await encryptor?.decrypt(data: encryptedData!) + + XCTAssertNotNil(decryptedData, "Decryption should succeed") + XCTAssertEqual(decryptedData, data, "Decrypted data should match original data") + } catch { + XCTFail("Decryption failed with error: \(error)") + } + } +} diff --git a/Storage/StorageTests/SecuredKeyTests.swift b/Storage/StorageTests/SecuredKeyTests.swift new file mode 100644 index 0000000..a9135ec --- /dev/null +++ b/Storage/StorageTests/SecuredKeyTests.swift @@ -0,0 +1,37 @@ +// +// SecuredKeyTests.swift +// StorageTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingStorage + +final class SecuredKeyTests: XCTestCase { + + private var securedKey: SecuredKey! + + override func setUp() { + super.setUp() + securedKey = SecuredKey(applicationTag: "com.pingidentity.securedKey.identifier") + } + + override func tearDown() { + securedKey = nil + super.tearDown() + } + + // TestRailCase(24709) + func testEncryptAndDecrypt() { + let data = "Test data".data(using: .utf8)! + let encryptedData = securedKey.encrypt(data: data) + XCTAssertNotNil(encryptedData) + let decryptedData = securedKey.decrypt(data: encryptedData!) + XCTAssertEqual(decryptedData, data) + } +} diff --git a/Storage/StorageTests/StorageDelegateTests.swift b/Storage/StorageTests/StorageDelegateTests.swift new file mode 100644 index 0000000..36e8d63 --- /dev/null +++ b/Storage/StorageTests/StorageDelegateTests.swift @@ -0,0 +1,162 @@ +// +// StorageDelegateTests.swift +// StorageTests +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import XCTest +@testable import PingStorage + +final class StorageDelegateTests: XCTestCase { + private var storageDelegate: StorageDelegate! + private var memoryStorage: MemoryStorage! + + override func setUp() { + super.setUp() + memoryStorage = MemoryStorage() + storageDelegate = StorageDelegate(delegate: memoryStorage, cacheable: false) + } + + override func tearDown() { + storageDelegate = nil + memoryStorage = nil + super.tearDown() + } + + func testSaveItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await storageDelegate.save(item: item) + let retrievedItem = try await storageDelegate.get() + XCTAssertEqual(retrievedItem, item) + } + + func testGetItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await storageDelegate.save(item: item) + let retrievedItem = try await storageDelegate.get() + XCTAssertEqual(retrievedItem, item) + } + + func testDeleteItem() async throws { + let item = TestItem(id: 1, name: "Test") + try await storageDelegate.save(item: item) + try await storageDelegate.delete() + let retrievedItem = try await storageDelegate.get() + XCTAssertNil(retrievedItem) + } + + func testConcurrentAccess() { + let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent) + let group = DispatchGroup() + let item = TestItem(id: 1, name: "Test") + let iterations = 1000 + + // Concurrent writes + for _ in 0..