├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── builds.yml │ ├── publish-docs.yml │ ├── scorecards.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Examples ├── Example-iOS │ ├── .gitignore │ ├── Example-iOS.xcodeproj │ │ └── project.pbxproj │ ├── Example-iOSForPod.xcodeproj │ │ └── project.pbxproj │ ├── Podfile │ ├── README.md │ └── Source │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ ├── GTMAppAuthExampleViewController.h │ │ ├── GTMAppAuthExampleViewController.m │ │ ├── GTMAppAuthExampleViewController.xib │ │ ├── Info.plist │ │ └── main.m └── Example-macOS │ ├── .gitignore │ ├── Example-macOS.entitlements │ ├── Example-macOS.xcodeproj │ └── project.pbxproj │ ├── Example-macOSForPod.xcodeproj │ └── project.pbxproj │ ├── Podfile │ ├── README.md │ └── Source │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── GTMAppAuthExampleViewController.xib │ └── MainMenu.xib │ ├── GTMAppAuthExampleViewController.h │ ├── GTMAppAuthExampleViewController.m │ ├── Info.plist │ └── main.m ├── GTMAppAuth.podspec ├── GTMAppAuth ├── Sources │ ├── AuthSession.swift │ ├── AuthSessionDelegate.swift │ ├── AuthSessionStore.swift │ ├── KeychainStore │ │ ├── GTMOAuth2Compatibility.swift │ │ ├── KeychainAttribute.swift │ │ ├── KeychainHelper.swift │ │ └── KeychainStore.swift │ └── Resources │ │ └── PrivacyInfo.xcprivacy └── Tests │ ├── Helpers │ ├── AuthorizationTestingHelp.swift │ ├── GTMAppAuthTestingHelpers.swift │ ├── KeychainHelperFake.swift │ ├── OIDAuthStateTesting.swift │ ├── OIDAuthorizationRequestTesting.swift │ ├── OIDAuthorizationResponseTesting.swift │ ├── OIDRegistrationRequestTesting.swift │ ├── OIDRegistrationResponseTesting.swift │ ├── OIDServiceConfigurationTesting.swift │ ├── OIDTokenRequestTesting.swift │ └── OIDTokenResponseTesting.swift │ ├── ObjCIntegration │ ├── GTMAuthSessionTests.m │ └── KeychainStore │ │ ├── GTMKeychainStoreTests.m │ │ └── GTMOAuth2CompatibilityTests.m │ └── Unit │ ├── AuthSessionTests.swift │ └── KeychainStore │ ├── GTMOAuth2CompatibilityTests.swift │ └── KeychainStoreTests.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift └── README.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Submit a bug report if something isn't working as expected. 4 | title: "" 5 | labels: bug, triage 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Tap on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environment** 26 | - Device: [ e.g. iPhone 13, MacBook Pro, etc ] 27 | - OS: [ e.g. iOS 15, macOS 11, etc ] 28 | - Browser: [ e.g. Safari, Chrome, etc ] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Make a feature request if you have a suggestion for something new. 4 | title: "" 5 | labels: enhancement, triage 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem you're having? Please describe.** 10 | A clear and concise description of what the problem is. 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/builds.yml: -------------------------------------------------------------------------------- 1 | name: Build GTMAppAuth for Valid Architectures 2 | 3 | on: 4 | schedule: 5 | - cron: '0 8 * * *' # Cron uses UTC; run at nightly at midnight PST 6 | workflow_dispatch: 7 | 8 | jobs: 9 | cron: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [macos-12] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Archive for iOS 19 | run: | 20 | xcodebuild \ 21 | archive \ 22 | -scheme GTMAppAuth \ 23 | -destination "generic/platform=iOS" 24 | - name: Archive for macOS 25 | run: | 26 | xcodebuild \ 27 | archive \ 28 | -scheme GTMAppAuth \ 29 | -destination "platform=OS X" 30 | - name: Archive for watchOS 31 | run: | 32 | xcodebuild \ 33 | archive \ 34 | -scheme GTMAppAuth \ 35 | -destination "generic/platform=watchOS" 36 | - name: Archive for tvOS 37 | run: | 38 | xcodebuild \ 39 | archive \ 40 | -scheme GTMAppAuth \ 41 | -destination "generic/platform=tvOS" 42 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish API Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - petea-docs 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: pages 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | 15 | generate: 16 | runs-on: macos-12 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | - name: Build DocC Archive 21 | run: | 22 | xcodebuild docbuild \ 23 | -scheme GTMAppAuth \ 24 | -derivedDataPath /tmp/xcodebuild_output \ 25 | -destination 'generic/platform=iOS' \ 26 | DOCC_EXTRACT_OBJC_INFO_FOR_SWIFT_SYMBOLS=YES \ 27 | SWIFT_INSTALL_OBJC_HEADER=YES 28 | - name: Generate Static Content 29 | run: | 30 | $(xcrun --find docc) process-archive transform-for-static-hosting \ 31 | /tmp/xcodebuild_output/Build/Products/Debug-iphoneos/GTMAppAuth.doccarchive \ 32 | --output-path /tmp/docs \ 33 | --hosting-base-path /GTMAppAuth/ 34 | - name: Insert Redirect 35 | run: | 36 | printf "%s\n" \ 37 | "" \ 38 | "" \ 39 | "" \ 40 | "" \ 41 | "" \ 42 | "" > /tmp/docs/index.html 43 | - name: Upload GitHub Pages Artifact 44 | uses: actions/upload-pages-artifact@v1 45 | with: 46 | path: /tmp/docs 47 | 48 | deploy: 49 | needs: generate 50 | permissions: 51 | pages: write 52 | id-token: write 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Deploy to GitHub Pages 59 | id: deployment 60 | uses: actions/deploy-pages@v1 61 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | # Only the default branch is supported. 4 | branch_protection_rule: 5 | schedule: 6 | - cron: '20 23 * * 4' 7 | push: 8 | branches: [ "master" ] 9 | 10 | # Declare default permissions as read only. 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecards analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | # Needed to upload the results to code-scanning dashboard. 19 | security-events: write 20 | # Used to receive a badge. 21 | id-token: write 22 | # Needs for private repositories. 23 | contents: read 24 | actions: read 25 | 26 | steps: 27 | - name: "Checkout code" 28 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3.0.0 29 | with: 30 | persist-credentials: false 31 | 32 | - name: "Run analysis" 33 | uses: ossf/scorecard-action@3e15ea8318eee9b333819ec77a36aca8d39df13e # tag=v1.1.1 34 | with: 35 | results_file: results.sarif 36 | results_format: sarif 37 | # (Optional) Read-only PAT token. Uncomment the `repo_token` line below if: 38 | # - you want to enable the Branch-Protection check on a *public* repository, or 39 | # - you are installing Scorecards on a *private* repository 40 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 41 | # repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} 42 | 43 | # Publish the results for public repositories to enable scorecard badges. For more details, see 44 | # https://github.com/ossf/scorecard-action#publishing-results. 45 | # For private repositories, `publish_results` will automatically be set to `false`, regardless 46 | # of the value entered here. 47 | publish_results: true 48 | 49 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 50 | # format to the repository Actions tab. 51 | - name: "Upload artifact" 52 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag=v4.6.2 53 | with: 54 | name: SARIF file 55 | path: results.sarif 56 | retention-days: 5 57 | 58 | # Upload the results to GitHub's code scanning dashboard. 59 | - name: "Upload to code-scanning" 60 | uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # tag=v1.0.26 61 | with: 62 | sarif_file: results.sarif 63 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | jobs: 13 | 14 | pod-lib-lint: 15 | runs-on: macos-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Update Bundler 19 | run: bundle update --bundler 20 | - name: Install Ruby gems with Bundler 21 | run: bundle install 22 | - name: Lint GTMAppAuth.podspec using local source 23 | run: pod lib lint GTMAppAuth.podspec --verbose 24 | 25 | spm-build-test: 26 | runs-on: macos-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Build package 30 | run: swift build 31 | - name: Run Swift tests 32 | run: swift test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS 2 | .DS_Store 3 | 4 | # CocoaPods 5 | Pods/ 6 | gen/ 7 | 8 | # Swift Package Manager 9 | Package.pins 10 | Package.resolved 11 | .swiftpm 12 | .build 13 | 14 | # Xcode 15 | *.xcuserstate 16 | *.xcworkspace/ 17 | project.xcworkspace/ 18 | xcuserdata/ 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 5.0.0 2 | - Update keychain default on Mac to be data protected ([#259](https://github.com/google/GTMAppAuth/pull/259)) 3 | - Update AppAuth dependency to 2.0.0 ([#262](https://github.com/google/GTMAppAuth/pull/262)) 4 | - Fix Swift 6 mode `@Sendable` closure warning ([#263](https://github.com/google/GTMAppAuth/pull/263)) 5 | 6 | # 4.1.1 7 | - Fix PrivacyInfo bundling issues ([#241](https://github.com/google/GTMAppAuth/pull/241)) 8 | 9 | # 4.1.0 10 | - Fix keychain key bug ([#237](https://github.com/google/GTMAppAuth/pull/237)) 11 | - Add privacy manifest ([#239](https://github.com/google/GTMAppAuth/pull/239)) 12 | 13 | # 4.0.0 14 | 15 | - Updated `AuthSessionDelegate` method for updating error to take a completion to support async updates [#229](https://github.com/google/GTMAppAuth/pull/229) 16 | 17 | # 3.0.0 18 | 19 | - GTMAppAuth has been translated from Objective-C to Swift. ([#190](https://github.com/google/GTMAppAuth/pull/190)) 20 | - Improved API surface. ([#203](https://github.com/google/GTMAppAuth/pull/203)) 21 | - Renamed `GTMAppAuthFetcherAuthorization` to `GTMAuthSession`. 22 | - Added `GTMAuthSessionStore` protocol. 23 | - Added `GTMKeychainStore` class conforming to `GTMAuthSessionStore` providing keychain storage of `GTMAuthSession` objects as well as [GTMOAuth2](https://github.com/google/gtm-oauth2) compatibility. 24 | - Added the ability to specify a keychain access group rather than use the default group. 25 | - Stored representation of `GTMAuthSession` objects remains unchanged from that of `GTMAppAuthFetcherAuthorization` 26 | in previous versions of GTMAppAuth, allowing for backward and forward compatibilty. 27 | - Significantly improved error handling. 28 | - Added [`AuthSessionDelegate`](https://github.com/google/GTMAppAuth/pull/224/files#diff-ad81e5244511faaa4b3d98553128438e30a94914c279fb7d42c0acfbbdc24500) protocol allowing clients to 1) provide additional token refresh parameters and 2) inspect and modify errors before they are returned during the authorize request flow. 29 | - Unit tests added. ([#190](https://github.com/google/GTMAppAuth/pull/190), [#202](https://github.com/google/GTMAppAuth/pull/202)) 30 | 31 | # 2.0.0 32 | 33 | * Updated the GTMSessionFetcher dependency to allow 3.x versions. ([#192](https://github.com/google/GTMAppAuth/pull/192)) 34 | * Minimum deployment versions for iOS and tvOS increased to 10. ([#188](https://github.com/google/GTMAppAuth/pull/188), [#191](https://github.com/google/GTMAppAuth/pull/191)) 35 | 36 | # 1.3.1 37 | 38 | * Updated the GTMSessionFetcher dependency to allow 2.x versions. ([#155](https://github.com/google/GTMAppAuth/pull/155), [#175](https://github.com/google/GTMAppAuth/pull/175)) 39 | * Use secure coding with `NSKeyedArchiver` when available. ([#145](https://github.com/google/GTMAppAuth/pull/145)) 40 | 41 | # 1.3.0 42 | 43 | * Added the option to use the data protection keychain on macOS. ([#151](https://github.com/google/GTMAppAuth/pull/151)) 44 | * Unified the keychain access layer, moving macOS to the modern SecItem API. ([#150](https://github.com/google/GTMAppAuth/pull/150)) 45 | * Added Swift Package Manager projects for the example apps. ([#153](https://github.com/google/GTMAppAuth/pull/153)) 46 | 47 | # 1.2.3 48 | 49 | * Fixed Keychain duplicate entry error on macOS. ([#138](https://github.com/google/GTMAppAuth/pull/138)) 50 | * Match GTMSessionFetcher's min macOS version to avoid warnings. ([#142](https://github.com/google/GTMAppAuth/pull/142)) 51 | 52 | # 1.2.2 53 | 54 | * Fixed Swift Package Manager issue with Xcode 12.5. 55 | 56 | # 1.2.1 57 | 58 | * Address CocoaPod packaging issue in the 1.2.0 release. 59 | 60 | # 1.2.0 61 | 62 | * Addressed several Swift Package Manager issues. 63 | * Restructured the project for cleaner Swift Package Manager support. 64 | 65 | # 1.1.0 66 | 67 | * Added Swift Package Manager support. 68 | * Added watchOS support. 69 | 70 | # 1.0.0 71 | 72 | * Moved tvOS authorization support out to a branch. 73 | 74 | # 0.8.0 75 | 76 | * Added `tokenRefreshDelegate` to `GTMAppAuthFetcherAuthorization`. 77 | * Updated to depend on AppAuth/Core 1.0. 78 | * Added CHANGELOG.md. 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement] 6 | (https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | Before you start working on a larger contribution, you should get in touch with 15 | us first through the issue tracker with your idea so that we can help out and 16 | possibly guide you. Coordinating up front makes it much easier to avoid 17 | frustration later on. 18 | 19 | ### Code reviews 20 | All submissions, including submissions by project members, require review. We 21 | use Github pull requests for this purpose. 22 | 23 | ### The small print 24 | Contributions made by corporations are covered by a different agreement than 25 | the one above, the 26 | [Software Grant and Corporate Contributor License Agreement] 27 | (https://cla.developers.google.com/about/google-corporate). -------------------------------------------------------------------------------- /Examples/Example-iOS/.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | 5 | ## Various settings 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | 16 | ## Other 17 | *.moved-aside 18 | *.xcuserstate 19 | 20 | # Pods are ignored in the samples as all Pods & their dependencies are either 21 | # development Pods (this repo) or sourced from repos in the same organization. 22 | # Generally we recommend versioning Pods, see the pros & cons here: 23 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 24 | Pods 25 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '12.0' 2 | 3 | target 'Example-iOSForPod' do 4 | use_frameworks! 5 | project 'Example-iOSForPod' 6 | 7 | # Pods for GTMAppAuth development 8 | pod 'GTMAppAuth', :path => '../../', :testspecs => ['unit', 'objc-api-integration'] 9 | 10 | # In production, you would use: 11 | # pod 'GTMAppAuth' 12 | 13 | pod 'AppAuth', '~> 2.0' 14 | pod 'GTMSessionFetcher/Core', '>= 3.3', '< 4.0' 15 | end 16 | -------------------------------------------------------------------------------- /Examples/Example-iOS/README.md: -------------------------------------------------------------------------------- 1 | # iOS Example 2 | 3 | ## Getting Started 4 | 5 | Choose which package manager you'd like to use. 6 | 7 | ### Swift Package Manager 8 | 9 | In the Example-iOS folder, run the following command to open the Swift Pacakage Manager 10 | project: 11 | 12 | ``` 13 | open Example-iOS.xcodeproj 14 | ``` 15 | 16 | ### CocoaPods 17 | 18 | 1. In the Example-iOS folder, run the following command to install the required 19 | library pods: 20 | 21 | ``` 22 | $ pod install 23 | ``` 24 | 25 | 2. Open the generated workspace for the CocoaPods project: 26 | 27 | ``` 28 | $ open Example-iOSForPod.xcworkspace 29 | ``` 30 | 31 | ## Configuration 32 | 33 | The example doesn't work out of the box, you need to configure it your own 34 | client ID. 35 | 36 | ### Creating a Google OAuth Client 37 | 38 | To configure the sample with a Google OAuth client, visit 39 | https://console.developers.google.com/apis/credentials?project=_ and create a 40 | new project. Then tap "Create credentials" and select "OAuth client ID". 41 | Follow the instructions to configure the consent screen (just the Product Name 42 | is needed). 43 | 44 | Then, complete the OAuth client creation by selecting "iOS" as the Application 45 | type. Enter the Bundle ID of the project (`net.openid.appauth.Example` by 46 | default, but you may want to change this in the project and use your own 47 | Bundle ID). 48 | 49 | Copy the client ID to the clipboard. 50 | 51 | ### Configure the Example 52 | 53 | In `GTMAppAuthExampleViewController.m` update `kClientID` with your new client 54 | id. 55 | 56 | In the same file, update `kRedirectURI` with the *reverse DNS notation* form 57 | of the client ID. For example, if the client ID is 58 | `YOUR_CLIENT.apps.googleusercontent.com`, the reverse DNS notation would be 59 | `com.googleusercontent.apps.YOUR_CLIENT`. A path component is added resulting in 60 | `com.googleusercontent.apps.YOUR_CLIENT:/oauthredirect`. 61 | 62 | Finally, open `Info.plist` and fully expand "URL types" (a.k.a. 63 | "CFBundleURLTypes") and replace `com.googleusercontent.apps.YOUR_CLIENT` with 64 | the reverse DNS notation form of your client id (not including the 65 | `:/oauthredirect` path component). 66 | 67 | Once you have made those three changes, the sample should be ready to try with 68 | your new OAuth client. 69 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /*! @file AppDelegate.h 2 | @brief GTMAppAuth SDK iOS Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | #import 19 | 20 | @protocol OIDExternalUserAgentSession; 21 | 22 | /*! @brief The example application's delegate. 23 | */ 24 | @interface AppDelegate : UIResponder 25 | 26 | /*! @brief The authorization flow session which receives the return URL from 27 | \SFSafariViewController. 28 | @discussion We need to store this in the app delegate as it's that delegate which receives the 29 | incoming URL on UIApplicationDelegate.application:openURL:options:. This property will be 30 | nil, except when an authorization flow is in progress. 31 | */ 32 | @property(nonatomic, strong, nullable) id currentAuthorizationFlow; 33 | 34 | @end 35 | 36 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /*! @file AppDelegate.m 2 | @brief GTMAppAuth SDK iOS Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | #import "AppDelegate.h" 19 | 20 | @import AppAuth; 21 | 22 | #import "GTMAppAuthExampleViewController.h" 23 | 24 | @implementation AppDelegate 25 | 26 | @synthesize window = _window; 27 | 28 | - (BOOL)application:(UIApplication *)application 29 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 30 | UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 31 | UIViewController *mainViewController = 32 | [[GTMAppAuthExampleViewController alloc] init]; 33 | window.rootViewController = mainViewController; 34 | 35 | _window = window; 36 | [_window makeKeyAndVisible]; 37 | 38 | return YES; 39 | } 40 | 41 | /*! @brief Handles inbound URLs. Checks if the URL matches the redirect URI for a pending 42 | AppAuth authorization request. 43 | */ 44 | - (BOOL)application:(UIApplication *)app 45 | openURL:(NSURL *)url 46 | options:(NSDictionary *)options { 47 | // Sends the URL to the current authorization flow (if any) which will process it if it relates to 48 | // an authorization response. 49 | if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { 50 | _currentAuthorizationFlow = nil; 51 | return YES; 52 | } 53 | 54 | // Your additional URL handling (if any) goes here. 55 | 56 | return NO; 57 | } 58 | 59 | /*! @brief Forwards inbound URLs for iOS 8.x and below to @c application:openURL:options:. 60 | @discussion When you drop support for versions of iOS earlier than 9.0, you can delete this 61 | method. NB. this implementation doesn't forward the sourceApplication or annotations. If you 62 | need these, then you may want @c application:openURL:options to call this method instead. 63 | */ 64 | - (BOOL)application:(UIApplication *)application 65 | openURL:(NSURL *)url 66 | sourceApplication:(NSString *)sourceApplication 67 | annotation:(id)annotation { 68 | return [self application:application 69 | openURL:url 70 | options:@{}]; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/GTMAppAuthExampleViewController.h: -------------------------------------------------------------------------------- 1 | /*! @file AppAuthExampleViewController.h 2 | @brief GTMAppAuth SDK iOS Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | #import 19 | 20 | @class OIDAuthState; 21 | @class GTMAuthSession; 22 | @class OIDServiceConfiguration; 23 | 24 | NS_ASSUME_NONNULL_BEGIN 25 | 26 | /*! @brief The example application's view controller. 27 | */ 28 | @interface GTMAppAuthExampleViewController : UIViewController 29 | 30 | @property(nullable) IBOutlet UIButton *authAutoButton; 31 | @property(nullable) IBOutlet UIButton *userinfoButton; 32 | @property(nullable) IBOutlet UIButton *clearAuthStateButton; 33 | @property(nullable) IBOutlet UITextView *logTextView; 34 | 35 | /*! @brief The auth session. 36 | */ 37 | @property(nonatomic, nullable) GTMAuthSession *authSession; 38 | 39 | /*! @brief Authorization code flow using @c OIDAuthState automatic code exchanges. 40 | @param sender IBAction sender. 41 | */ 42 | - (IBAction)authWithAutoCodeExchange:(nullable id)sender; 43 | 44 | /*! @brief Performs a Userinfo API call using @c GTMAuthSession. 45 | @param sender IBAction sender. 46 | */ 47 | - (IBAction)userinfo:(nullable id)sender; 48 | 49 | /*! @brief Nils the @c authSession. 50 | @param sender IBAction sender. 51 | */ 52 | - (IBAction)clearAuthState:(nullable id)sender; 53 | 54 | /*! @brief Clears the UI log. 55 | @param sender IBAction sender. 56 | */ 57 | - (IBAction)clearLog:(nullable id)sender; 58 | 59 | @end 60 | 61 | NS_ASSUME_NONNULL_END 62 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/GTMAppAuthExampleViewController.m: -------------------------------------------------------------------------------- 1 | /*! @file AppAuthExampleViewController.m 2 | @brief GTMAppAuth SDK iOS Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | #import "GTMAppAuthExampleViewController.h" 20 | 21 | #import 22 | 23 | @import AppAuth; 24 | @import GTMAppAuth; 25 | #ifdef COCOAPODS 26 | @import GTMSessionFetcher; 27 | #else // SPM 28 | @import GTMSessionFetcherCore; 29 | #endif 30 | 31 | #import "AppDelegate.h" 32 | 33 | /*! @brief The OIDC issuer from which the configuration will be discovered. 34 | */ 35 | static NSString *const kIssuer = @"https://accounts.google.com"; 36 | 37 | /*! @brief The OAuth client ID. 38 | @discussion For Google, register your client at 39 | https://console.developers.google.com/apis/credentials?project=_ 40 | The client should be registered with the "iOS" type. 41 | */ 42 | static NSString *const kClientID = @"YOUR_CLIENT.apps.googleusercontent.com"; 43 | 44 | /*! @brief The OAuth redirect URI for the client @c kClientID. 45 | @discussion With Google, the scheme of the redirect URI is the reverse DNS notation of the 46 | client ID. This scheme must be registered as a scheme in the project's Info 47 | property list ("CFBundleURLTypes" plist key). Any path component will work, we use 48 | 'oauthredirect' here to help disambiguate from any other use of this scheme. 49 | */ 50 | static NSString *const kRedirectURI = 51 | @"com.googleusercontent.apps.YOUR_CLIENT:/oauthredirect"; 52 | 53 | /*! @brief The key used to store our `GTMAuthSession` in the keychain. 54 | */ 55 | static NSString *const kKeychainStoreItemName = @"authorization"; 56 | 57 | @interface GTMAppAuthExampleViewController () 59 | 60 | @property (nonatomic, strong) GTMKeychainStore *keychainStore; 61 | 62 | @end 63 | 64 | @implementation GTMAppAuthExampleViewController 65 | 66 | - (void)viewDidLoad { 67 | [super viewDidLoad]; 68 | 69 | self.keychainStore = [[GTMKeychainStore alloc] initWithItemName:kKeychainStoreItemName]; 70 | #if !defined(NS_BLOCK_ASSERTIONS) 71 | // NOTE: 72 | // 73 | // To run this sample, you need to register your own iOS client at 74 | // https://console.developers.google.com/apis/credentials?project=_ and update three configuration 75 | // points in the sample: kClientID and kRedirectURI constants in AppAuthExampleViewController.m 76 | // and the URI scheme in Info.plist (URL Types -> Item 0 -> URL Schemes -> Item 0). 77 | // Full instructions: https://github.com/openid/AppAuth-iOS/blob/master/Example/README.md 78 | 79 | NSAssert(![kClientID isEqualToString:@"YOUR_CLIENT.apps.googleusercontent.com"], 80 | @"Update kClientID with your own client ID. " 81 | "Instructions: https://github.com/openid/AppAuth-iOS/blob/master/Example/README.md"); 82 | 83 | NSAssert(![kRedirectURI isEqualToString:@"com.googleusercontent.apps.YOUR_CLIENT:/oauthredirect"], 84 | @"Update kRedirectURI with your own redirect URI. " 85 | "Instructions: https://github.com/openid/AppAuth-iOS/blob/master/Example/README.md"); 86 | 87 | // verifies that the custom URI scheme has been updated in the Info.plist 88 | NSArray *urlTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; 89 | NSAssert(urlTypes.count > 0, @"No custom URI scheme has been configured for the project."); 90 | NSArray *urlSchemes = ((NSDictionary *)urlTypes.firstObject)[@"CFBundleURLSchemes"]; 91 | NSAssert(urlSchemes.count > 0, @"No custom URI scheme has been configured for the project."); 92 | NSString *urlScheme = urlSchemes.firstObject; 93 | 94 | NSAssert(![urlScheme isEqualToString:@"com.googleusercontent.apps.YOUR_CLIENT"], 95 | @"Configure the URI scheme in Info.plist (URL Types -> Item 0 -> URL Schemes -> Item 0) " 96 | "with the scheme of your redirect URI. Full instructions: " 97 | "https://github.com/openid/AppAuth-iOS/blob/master/Example/README.md"); 98 | 99 | #endif // !defined(NS_BLOCK_ASSERTIONS) 100 | 101 | _logTextView.layer.borderColor = [UIColor colorWithWhite:0.8 alpha:1.0].CGColor; 102 | _logTextView.layer.borderWidth = 1.0f; 103 | _logTextView.alwaysBounceVertical = YES; 104 | _logTextView.textContainer.lineBreakMode = NSLineBreakByCharWrapping; 105 | _logTextView.text = @""; 106 | 107 | [self loadState]; 108 | [self updateUI]; 109 | } 110 | 111 | /*! @brief Saves the @c GTMAuthSession to the keychain. 112 | */ 113 | - (void)saveState { 114 | NSError *error; 115 | if (_authSession.canAuthorize) { 116 | [self.keychainStore saveAuthSession:_authSession error:&error]; 117 | } else { 118 | [self.keychainStore removeAuthSessionWithError:&error]; 119 | } 120 | if (error) { 121 | NSLog(@"Error saving state: %@ error", error); 122 | } 123 | } 124 | 125 | /*! @brief Loads the @c GTMAuthSession from the keychain. 126 | */ 127 | - (void)loadState { 128 | NSError *error; 129 | GTMAuthSession *authSession = 130 | [self.keychainStore retrieveAuthSessionWithError:&error]; 131 | if (error) { 132 | NSLog(@"Error loading state: %@", error); 133 | } 134 | [self setAuthSession:authSession]; 135 | } 136 | 137 | - (void)setAuthSession:(GTMAuthSession *)authSession { 138 | if ([_authSession isEqual:authSession]) { 139 | return; 140 | } 141 | _authSession = authSession; 142 | [self stateChanged]; 143 | } 144 | 145 | /*! @brief Refreshes UI, typically called after the auth state changed. 146 | */ 147 | - (void)updateUI { 148 | _userinfoButton.enabled = _authSession.canAuthorize; 149 | _clearAuthStateButton.enabled = _authSession.canAuthorize; 150 | // dynamically changes authorize button text depending on authorized state 151 | if (!_authSession.canAuthorize) { 152 | [_authAutoButton setTitle:@"Authorize" forState:UIControlStateNormal]; 153 | [_authAutoButton setTitle:@"Authorize" forState:UIControlStateHighlighted]; 154 | } else { 155 | [_authAutoButton setTitle:@"Re-authorize" forState:UIControlStateNormal]; 156 | [_authAutoButton setTitle:@"Re-authorize" forState:UIControlStateHighlighted]; 157 | } 158 | } 159 | 160 | - (void)stateChanged { 161 | [self saveState]; 162 | [self updateUI]; 163 | } 164 | 165 | - (void)didChangeState:(OIDAuthState *)state { 166 | [self stateChanged]; 167 | } 168 | 169 | - (void)authState:(OIDAuthState *)state didEncounterAuthorizationError:(NSError *)error { 170 | [self logMessage:@"Received authorization error: %@", error]; 171 | } 172 | 173 | - (IBAction)authWithAutoCodeExchange:(nullable id)sender { 174 | NSURL *issuer = [NSURL URLWithString:kIssuer]; 175 | NSURL *redirectURI = [NSURL URLWithString:kRedirectURI]; 176 | 177 | [self logMessage:@"Fetching configuration for issuer: %@", issuer]; 178 | 179 | // discovers endpoints 180 | [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer 181 | completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { 182 | 183 | if (!configuration) { 184 | [self logMessage:@"Error retrieving discovery document: %@", [error localizedDescription]]; 185 | [self setAuthSession:nil]; 186 | return; 187 | } 188 | 189 | [self logMessage:@"Got configuration: %@", configuration]; 190 | 191 | // builds authentication request 192 | OIDAuthorizationRequest *request = 193 | [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration 194 | clientId:kClientID 195 | scopes:@[OIDScopeOpenID, OIDScopeProfile] 196 | redirectURL:redirectURI 197 | responseType:OIDResponseTypeCode 198 | additionalParameters:nil]; 199 | // performs authentication request 200 | AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; 201 | [self logMessage:@"Initiating authorization request with scope: %@", request.scope]; 202 | 203 | appDelegate.currentAuthorizationFlow = 204 | [OIDAuthState authStateByPresentingAuthorizationRequest:request 205 | presentingViewController:self 206 | callback:^(OIDAuthState *_Nullable authState, 207 | NSError *_Nullable error) { 208 | if (authState) { 209 | GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:authState]; 210 | 211 | [self setAuthSession:authSession]; 212 | [self logMessage:@"Got authorization tokens. Access token: %@", 213 | authState.lastTokenResponse.accessToken]; 214 | } else { 215 | [self setAuthSession:nil]; 216 | [self logMessage:@"Authorization error: %@", [error localizedDescription]]; 217 | } 218 | }]; 219 | }]; 220 | } 221 | 222 | - (IBAction)clearAuthState:(nullable id)sender { 223 | [self setAuthSession:nil]; 224 | } 225 | 226 | - (IBAction)clearLog:(nullable id)sender { 227 | _logTextView.text = @""; 228 | } 229 | 230 | - (IBAction)userinfo:(nullable id)sender { 231 | [self logMessage:@"Performing userinfo request"]; 232 | 233 | // Creates a GTMSessionFetcherService with the authorization. 234 | // Normally you would save this service object and re-use it for all REST API calls. 235 | GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init]; 236 | fetcherService.authorizer = self.authSession; 237 | 238 | // Creates a fetcher for the API call. 239 | NSURL *userinfoEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v3/userinfo"]; 240 | GTMSessionFetcher *fetcher = [fetcherService fetcherWithURL:userinfoEndpoint]; 241 | [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { 242 | // Checks for an error. 243 | if (error) { 244 | // OIDOAuthTokenErrorDomain indicates an issue with the authorization. 245 | if ([error.domain isEqual:OIDOAuthTokenErrorDomain]) { 246 | [self setAuthSession:nil]; 247 | [self logMessage:@"Authorization error during token refresh, clearing state. %@", error]; 248 | // Other errors are assumed transient. 249 | } else { 250 | [self logMessage:@"Transient error during token refresh. %@", error]; 251 | } 252 | return; 253 | } 254 | 255 | // Parses the JSON response. 256 | NSError *jsonError = nil; 257 | id jsonDictionaryOrArray = 258 | [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; 259 | 260 | // JSON error. 261 | if (jsonError) { 262 | [self logMessage:@"JSON decoding error %@", jsonError]; 263 | return; 264 | } 265 | 266 | // Success response! 267 | [self logMessage:@"Success: %@", jsonDictionaryOrArray]; 268 | }]; 269 | } 270 | 271 | /*! @brief Logs a message to stdout and the textfield. 272 | @param format The format string and arguments. 273 | */ 274 | - (void)logMessage:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) { 275 | // gets message as string 276 | va_list argp; 277 | va_start(argp, format); 278 | NSString *log = [[NSString alloc] initWithFormat:format arguments:argp]; 279 | va_end(argp); 280 | 281 | // outputs to stdout 282 | NSLog(@"%@", log); 283 | 284 | // appends to output log 285 | NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 286 | dateFormatter.dateFormat = @"hh:mm:ss"; 287 | NSString *dateString = [dateFormatter stringFromDate:[NSDate date]]; 288 | _logTextView.text = [NSString stringWithFormat:@"%@%@%@: %@", 289 | _logTextView.text, 290 | ([_logTextView.text length] > 0) ? @"\n" : @"", 291 | dateString, 292 | log]; 293 | } 294 | 295 | @end 296 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/GTMAppAuthExampleViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 45 | 46 | 47 | 48 | 58 | 65 | 71 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLSchemes 27 | 28 | com.googleusercontent.apps.YOUR_CLIENT 29 | 30 | 31 | 32 | CFBundleVersion 33 | 1 34 | LSRequiresIPhoneOS 35 | 36 | UILaunchStoryboardName 37 | LaunchScreen 38 | UIRequiredDeviceCapabilities 39 | 40 | armv7 41 | 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Examples/Example-iOS/Source/main.m: -------------------------------------------------------------------------------- 1 | /*! @file main.m 2 | @brief GTMAppAuth SDK iOS Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | #import 20 | 21 | #import "AppDelegate.h" 22 | 23 | /*! @brief Application main entry point. 24 | @return Exit code. 25 | */ 26 | int main(int argc, char *argv[]) { 27 | @autoreleasepool { 28 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Examples/Example-macOS/.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | 5 | ## Various settings 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | 16 | ## Other 17 | *.moved-aside 18 | *.xcuserstate 19 | 20 | # Pods are ignored in the samples as all Pods & their dependencies are either 21 | # development Pods (this repo) or sourced from repos in the same organization. 22 | # Generally we recommend versioning Pods, see the pros & cons here: 23 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 24 | Pods 25 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Example-macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | $(AppIdentifierPrefix)com.example.GTMAppAuth.Example-macOS.test-group 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.12' 2 | 3 | target 'Example-macOSForPod' do 4 | use_frameworks! 5 | project 'Example-macOSForPod' 6 | 7 | # Pods for GTMAppAuth development 8 | pod 'GTMAppAuth', :path => '../../', :testspecs => ['unit', 'objc-api-integration'] 9 | 10 | # In production, you would use: 11 | # pod 'GTMAppAuth' 12 | 13 | pod 'AppAuth', '~> 2.0' 14 | pod 'GTMSessionFetcher/Core', '>= 3.3', '< 4.0' 15 | end 16 | -------------------------------------------------------------------------------- /Examples/Example-macOS/README.md: -------------------------------------------------------------------------------- 1 | # macOS Example 2 | 3 | ## Getting Started 4 | 5 | Choose which package manager you'd like to use. 6 | 7 | ### Swift Package Manager 8 | 9 | In the Example-macOS folder, run the following command to open the Swift Pacakage Manager 10 | project: 11 | 12 | ``` 13 | open Example-macOS.xcodeproj 14 | ``` 15 | 16 | ### CocoaPods 17 | 18 | 1. In the Example-macOS folder, run the following command to install the required 19 | library pods: 20 | 21 | ``` 22 | $ pod install 23 | ``` 24 | 25 | 2. Open the generated workspace for the CocoaPods project: 26 | 27 | ``` 28 | $ open Example-macOSForPod.xcworkspace 29 | ``` 30 | 31 | ## Configuration 32 | 33 | The example doesn't work out of the box, you need to configure your own client 34 | ID. 35 | 36 | ### Creating a Google OAuth Client 37 | 38 | To configure the sample with a Google OAuth client, visit 39 | https://console.developers.google.com/apis/credentials?project=_ and create a 40 | new project. Then tap "Create credentials" and select "OAuth client ID". 41 | Follow the instructions to configure the consent screen (just the Product Name 42 | is needed). 43 | 44 | Then, complete the OAuth client creation by selecting "iOS" as the Application 45 | type. 46 | 47 | Copy the client ID to the clipboard. 48 | 49 | ### Configure the Example 50 | 51 | In `GTMAppAuthExampleViewController.m` update `kClientID` with your new client 52 | ID. 53 | 54 | In the same file, update `kRedirectURI` with the *reverse DNS notation* form 55 | of the client ID. For example, if the client ID is 56 | `YOUR_CLIENT.apps.googleusercontent.com`, the reverse DNS notation would be 57 | `com.googleusercontent.apps.YOUR_CLIENT`. A path component is added resulting in 58 | `com.googleusercontent.apps.YOUR_CLIENT:/oauthredirect`. 59 | 60 | Finally, open `Info.plist` and fully expand "URL types" (a.k.a. 61 | "CFBundleURLTypes") and replace `com.googleusercontent.apps.YOUR_CLIENT` with 62 | the reverse DNS notation form of your client ID (not including the 63 | `:/oauthredirect` path component). 64 | 65 | Once you have made those three changes, the sample should be ready to try with 66 | your new OAuth client. 67 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /*! @file AppDelegate.h 2 | @brief GTMAppAuth macOS SDK Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | #import 20 | 21 | NS_ASSUME_NONNULL_BEGIN 22 | 23 | @protocol OIDExternalUserAgentSession; 24 | 25 | /*! @class AppDelegate 26 | @brief The example application's delegate. 27 | */ 28 | @interface AppDelegate : NSObject 29 | 30 | /*! @property currentAuthorizationFlow 31 | @brief The authorization flow session which receives the return URL from the browser. 32 | @discussion We need to store this in the app delegate as it's that delegate which receives the 33 | incoming URL. This property will be nil, except when an authorization flow is in progress. 34 | */ 35 | @property(nonatomic, strong, nullable) id currentAuthorizationFlow; 36 | 37 | @end 38 | 39 | NS_ASSUME_NONNULL_END 40 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /*! @file AppDelegate.m 2 | @brief GTMAppAuth macOS SDK Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | #import "AppDelegate.h" 19 | 20 | @import AppAuth; 21 | 22 | #import "GTMAppAuthExampleViewController.h" 23 | 24 | NS_ASSUME_NONNULL_BEGIN 25 | 26 | @interface AppDelegate () 27 | @property(nullable) IBOutlet NSWindow *window; 28 | @end 29 | 30 | @implementation AppDelegate 31 | 32 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 33 | _window.title = @"AppAuth Example for macOS"; 34 | GTMAppAuthExampleViewController *contentViewController = 35 | [[GTMAppAuthExampleViewController alloc] init]; 36 | contentViewController.appDelegate = self; 37 | _window.contentViewController = contentViewController; 38 | 39 | // Register for GetURL events. 40 | NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; 41 | [appleEventManager setEventHandler:self 42 | andSelector:@selector(handleGetURLEvent:withReplyEvent:) 43 | forEventClass:kInternetEventClass 44 | andEventID:kAEGetURL]; 45 | } 46 | 47 | - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event 48 | withReplyEvent:(NSAppleEventDescriptor *)replyEvent { 49 | NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; 50 | NSURL *URL = [NSURL URLWithString:URLString]; 51 | [_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:URL]; 52 | } 53 | 54 | @end 55 | 56 | NS_ASSUME_NONNULL_END 57 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/Base.lproj/GTMAppAuthExampleViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 35 | 45 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 84 | 85 | 86 | 87 | 88 | 89 | 99 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/GTMAppAuthExampleViewController.h: -------------------------------------------------------------------------------- 1 | /*! @file AppAuthExampleViewController.h 2 | @brief GTMAppAuth macOS SDK Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | #import 19 | 20 | @class AppDelegate; 21 | @class GTMAuthSession; 22 | @class OIDServiceConfiguration; 23 | 24 | NS_ASSUME_NONNULL_BEGIN 25 | 26 | /*! @brief The example application's view controller. 27 | */ 28 | @interface GTMAppAuthExampleViewController : NSViewController 29 | 30 | @property(nullable) IBOutlet NSButton *authAutoButton; 31 | @property(nullable) IBOutlet NSButton *userinfoButton; 32 | @property(nullable) IBOutlet NSButton *forceRefreshButton; 33 | @property(nullable) IBOutlet NSButton *clearAuthStateButton; 34 | @property(nullable) IBOutlet NSTextView *logTextView; 35 | 36 | /*! @brief The application delegate. This is used to store the current authorization flow. 37 | */ 38 | @property (nonatomic, weak, nullable) AppDelegate *appDelegate; 39 | 40 | /*! @brief The auth session. 41 | */ 42 | @property(nonatomic, nullable) GTMAuthSession *authSession; 43 | 44 | /*! @brief Authorization code flow using @c OIDAuthState automatic code exchanges. 45 | @param sender IBAction sender. 46 | */ 47 | - (IBAction)authWithAutoCodeExchange:(nullable id)sender; 48 | 49 | /*! @brief Forces a token refresh. 50 | @param sender IBAction sender. 51 | */ 52 | - (IBAction)forceRefresh:(nullable id)sender; 53 | 54 | /*! @brief Performs a Userinfo API call using @c GTMAuthSession. 55 | @param sender IBAction sender. 56 | */ 57 | - (IBAction)userinfo:(nullable id)sender; 58 | 59 | /*! @brief Nils the @c authSession. 60 | @param sender IBAction sender. 61 | */ 62 | - (IBAction)clearAuthState:(nullable id)sender; 63 | 64 | /*! @brief Clears the UI log. 65 | @param sender IBAction sender. 66 | */ 67 | - (IBAction)clearLog:(nullable id)sender; 68 | 69 | @end 70 | 71 | NS_ASSUME_NONNULL_END 72 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/GTMAppAuthExampleViewController.m: -------------------------------------------------------------------------------- 1 | /*! @file AppAuthExampleViewController.m 2 | @brief GTMAppAuth macOS SDK Example 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | #import "GTMAppAuthExampleViewController.h" 20 | 21 | #import 22 | 23 | @import AppAuth; 24 | @import GTMAppAuth; 25 | #ifdef COCOAPODS 26 | @import GTMSessionFetcher; 27 | #else // SPM 28 | @import GTMSessionFetcherCore; 29 | #endif 30 | 31 | #import "AppDelegate.h" 32 | 33 | /*! @brief The bundle ID will use in constructing the app group string for keychain queries. 34 | @discussion The string here is a combination of this example app's bundle ID and the keychain 35 | access group name added in the app's entitlements file. 36 | */ 37 | static NSString *kBundleIDAccessGroup = @"com.example.GTMAppAuth.Example-macOS.test-group"; 38 | 39 | /*! @brief The team ID you will use in constructing the app group string for keychain queries. 40 | @discussion The team ID you will use can be found in your developer team profile page on 41 | developer.apple.com. 42 | */ 43 | static NSString *const kTeamIDPrefix = @"YOUR_TEAM_ID"; 44 | 45 | /*! @brief The OIDC issuer from which the configuration will be discovered. 46 | */ 47 | static NSString *const kIssuer = @"https://accounts.google.com"; 48 | 49 | /*! @brief The OAuth client ID. 50 | @discussion For Google, register your client at 51 | https://console.developers.google.com/apis/credentials?project=_ 52 | */ 53 | static NSString *const kClientID = @"YOUR_CLIENT.apps.googleusercontent.com"; 54 | 55 | /*! @brief The OAuth redirect URI for the client @c kClientID. 56 | @discussion With Google, the scheme of the redirect URI is the reverse DNS notation of the 57 | client ID. This scheme must be registered as a scheme in the project's Info 58 | property list ("CFBundleURLTypes" plist key). Any path component will work, we use 59 | 'oauthredirect' here to help disambiguate from any other use of this scheme. 60 | */ 61 | static NSString *const kRedirectURI = 62 | @"com.googleusercontent.apps.YOUR_CLIENT:/oauthredirect"; 63 | 64 | /*! @brief NSCoding key for the authorization property. 65 | */ 66 | static NSString *const kExampleAuthorizerKey = @"authorization"; 67 | 68 | @interface GTMAppAuthExampleViewController () 70 | 71 | @property (nonatomic, strong) GTMKeychainStore *keychainStore; 72 | 73 | @end 74 | 75 | @implementation GTMAppAuthExampleViewController 76 | 77 | - (void)viewDidLoad { 78 | [super viewDidLoad]; 79 | 80 | NSString *testGroup = [NSString stringWithFormat:@"%@.%@", kTeamIDPrefix, kBundleIDAccessGroup]; 81 | GTMKeychainAttribute *accessGroup = [GTMKeychainAttribute keychainAccessGroupWithName:testGroup]; 82 | NSSet *attributes = [NSSet setWithArray:@[accessGroup]]; 83 | self.keychainStore = [[GTMKeychainStore alloc] initWithItemName:kExampleAuthorizerKey 84 | keychainAttributes:attributes]; 85 | #if !defined(NS_BLOCK_ASSERTIONS) 86 | 87 | NSAssert(![kTeamIDPrefix isEqualToString:@"YOUR_TEAM_ID"], 88 | @"Update kTeamIDPrefix with your own team ID."); 89 | 90 | // NOTE: 91 | // 92 | // To run this sample, you need to register your own Google API client at 93 | // https://console.developers.google.com/apis/credentials?project=_ and update three configuration 94 | // points in the sample: kClientID and kRedirectURI constants in AppAuthExampleViewController.m 95 | // and the URI scheme in Info.plist (URL Types -> Item 0 -> URL Schemes -> Item 0). 96 | // Full instructions: https://github.com/openid/AppAuth-iOS/blob/master/Example-Mac/README.md 97 | 98 | NSAssert(![kClientID isEqualToString:@"YOUR_CLIENT.apps.googleusercontent.com"], 99 | @"Update kClientID with your own client ID. " 100 | "Instructions: https://github.com/openid/AppAuth-iOS/blob/master/Example-Mac/README.md"); 101 | 102 | NSAssert(![kRedirectURI isEqualToString:@"com.googleusercontent.apps.YOUR_CLIENT:/oauthredirect"], 103 | @"Update kRedirectURI with your own redirect URI. " 104 | "Instructions: https://github.com/openid/AppAuth-iOS/blob/master/Example-Mac/README.md"); 105 | 106 | // verifies that the custom URI scheme has been updated in the Info.plist 107 | NSArray *urlTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; 108 | NSAssert(urlTypes.count > 0, @"No custom URI scheme has been configured for the project."); 109 | NSArray *urlSchemes = ((NSDictionary *)urlTypes.firstObject)[@"CFBundleURLSchemes"]; 110 | NSAssert(urlSchemes.count > 0, @"No custom URI scheme has been configured for the project."); 111 | NSString *urlScheme = urlSchemes.firstObject; 112 | 113 | NSAssert(![urlScheme isEqualToString:@"com.googleusercontent.apps.YOUR_CLIENT"], 114 | @"Configure the URI scheme in Info.plist (URL Types -> Item 0 -> URL Schemes -> Item 0) " 115 | "with the scheme of your redirect URI. Full instructions: " 116 | "https://github.com/openid/AppAuth-iOS/blob/master/Example-Mac/README.md"); 117 | 118 | #endif // !defined(NS_BLOCK_ASSERTIONS) 119 | 120 | _logTextView.layer.borderColor = [NSColor colorWithWhite:0.8 alpha:1.0].CGColor; 121 | _logTextView.layer.borderWidth = 1.0f; 122 | _logTextView.textContainer.lineBreakMode = NSLineBreakByCharWrapping; 123 | 124 | [self loadState]; 125 | [self updateUI]; 126 | } 127 | 128 | /*! @brief Saves the @c GTMAuthSession to the keychain. 129 | */ 130 | - (void)saveState { 131 | NSError *error; 132 | if (_authSession.canAuthorize) { 133 | [self.keychainStore saveAuthSession:_authSession error:&error]; 134 | } else { 135 | [self.keychainStore removeAuthSessionWithError:&error]; 136 | } 137 | if (error) { 138 | NSLog(@"Error saving state: %@", error); 139 | } 140 | } 141 | 142 | /*! @brief Loads the @c GTMAuthSession from the keychain. 143 | */ 144 | - (void)loadState { 145 | NSError *error; 146 | GTMAuthSession *authSession = [self.keychainStore retrieveAuthSessionWithError:&error]; 147 | [self setAuthSession:authSession]; 148 | if (error) { 149 | NSLog(@"Error loading state: %@", error); 150 | } 151 | } 152 | 153 | /*! @brief Refreshes UI, typically called after the auth state changed. 154 | */ 155 | - (void)updateUI { 156 | _userinfoButton.enabled = [_authSession canAuthorize]; 157 | _forceRefreshButton.enabled = [_authSession canAuthorize]; 158 | _clearAuthStateButton.enabled = _authSession != nil; 159 | // dynamically changes authorize button text depending on authorized state 160 | if (!_authSession) { 161 | _authAutoButton.title = @"Authorize"; 162 | } else { 163 | _authAutoButton.title = @"Re-authorize"; 164 | } 165 | } 166 | 167 | /*! @brief Forces a token refresh. 168 | @param sender IBAction sender. 169 | */ 170 | - (IBAction)forceRefresh:(nullable id)sender { 171 | [_authSession.authState setNeedsTokenRefresh]; 172 | } 173 | 174 | - (void)stateChanged { 175 | [self saveState]; 176 | [self updateUI]; 177 | } 178 | 179 | - (void)didChangeState:(OIDAuthState *)state { 180 | // TODO(wdenniss): update for GTMAppAuth 181 | [self stateChanged]; 182 | } 183 | 184 | - (void)setAuthSession:(GTMAuthSession *)authSession { 185 | _authSession = authSession; 186 | [self saveState]; 187 | [self updateUI]; 188 | } 189 | 190 | 191 | - (void)authState:(OIDAuthState *)state didEncounterAuthorizationError:(nonnull NSError *)error { 192 | [self logMessage:@"Received authorization error: %@", error]; 193 | } 194 | 195 | - (IBAction)authWithAutoCodeExchange:(nullable id)sender { 196 | NSURL *issuer = [NSURL URLWithString:kIssuer]; 197 | NSURL *redirectURI = [NSURL URLWithString:kRedirectURI]; 198 | 199 | [self logMessage:@"Fetching configuration for issuer: %@", issuer]; 200 | 201 | // discovers endpoints 202 | [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer 203 | completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { 204 | 205 | if (!configuration) { 206 | [self logMessage:@"Error retrieving discovery document: %@", [error localizedDescription]]; 207 | [self setAuthSession:nil]; 208 | return; 209 | } 210 | 211 | [self logMessage:@"Got configuration: %@", configuration]; 212 | 213 | // builds authentication request 214 | OIDAuthorizationRequest *request = 215 | [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration 216 | clientId:kClientID 217 | scopes:@[OIDScopeOpenID, OIDScopeProfile] 218 | redirectURL:redirectURI 219 | responseType:OIDResponseTypeCode 220 | additionalParameters:nil]; 221 | // performs authentication request 222 | self.appDelegate.currentAuthorizationFlow = 223 | [OIDAuthState authStateByPresentingAuthorizationRequest:request 224 | presentingWindow:self.view.window 225 | callback:^(OIDAuthState *_Nullable authState, 226 | NSError *_Nullable error) { 227 | if (authState) { 228 | GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:authState]; 229 | [self setAuthSession:authSession]; 230 | [self logMessage:@"Got authorization tokens. Access token: %@", 231 | authState.lastTokenResponse.accessToken]; 232 | } else { 233 | [self setAuthSession:nil]; 234 | [self logMessage:@"Authorization error: %@", [error localizedDescription]]; 235 | } 236 | }]; 237 | }]; 238 | } 239 | 240 | - (IBAction)clearAuthState:(nullable id)sender { 241 | [self setAuthSession:nil]; 242 | } 243 | 244 | - (IBAction)clearLog:(nullable id)sender { 245 | [_logTextView.textStorage setAttributedString:[[NSAttributedString alloc] initWithString:@""]]; 246 | } 247 | 248 | - (IBAction)userinfo:(nullable id)sender { 249 | [self logMessage:@"Performing userinfo request"]; 250 | 251 | // Creates a GTMSessionFetcherService with the authorization. 252 | // Normally you would save this service object and re-use it for all REST API calls. 253 | GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init]; 254 | fetcherService.authorizer = self.authSession; 255 | 256 | // Creates a fetcher for the API call. 257 | NSURL *userinfoEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v3/userinfo"]; 258 | GTMSessionFetcher *fetcher = [fetcherService fetcherWithURL:userinfoEndpoint]; 259 | [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { 260 | 261 | // Checks for an error. 262 | if (error) { 263 | // OIDOAuthTokenErrorDomain indicates an issue with the authorization. 264 | if ([error.domain isEqual:OIDOAuthTokenErrorDomain]) { 265 | [self setAuthSession:nil]; 266 | [self logMessage:@"Authorization error during token refresh, clearing state. %@", error]; 267 | // Other errors are assumed transient. 268 | } else { 269 | [self logMessage:@"Transient error during token refresh. %@", error]; 270 | } 271 | return; 272 | } 273 | 274 | // Parses the JSON response. 275 | NSError *jsonError = nil; 276 | id jsonDictionaryOrArray = 277 | [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; 278 | 279 | // JSON error. 280 | if (jsonError) { 281 | [self logMessage:@"JSON decoding error %@", jsonError]; 282 | return; 283 | } 284 | 285 | // Success response! 286 | [self logMessage:@"Success: %@", jsonDictionaryOrArray]; 287 | }]; 288 | } 289 | 290 | /*! @brief Logs a message to stdout and the textfield. 291 | @param format The format string and arguments. 292 | */ 293 | - (void)logMessage:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) { 294 | // gets message as string 295 | va_list argp; 296 | va_start(argp, format); 297 | NSString *log = [[NSString alloc] initWithFormat:format arguments:argp]; 298 | va_end(argp); 299 | 300 | // outputs to stdout 301 | NSLog(@"%@", log); 302 | 303 | // appends to output log 304 | NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 305 | dateFormatter.dateFormat = @"hh:mm:ss"; 306 | NSString *dateString = [dateFormatter stringFromDate:[NSDate date]]; 307 | NSString *logLine = [NSString stringWithFormat:@"\n%@: %@", dateString, log]; 308 | NSAttributedString* logLineAttr = [[NSAttributedString alloc] initWithString:logLine]; 309 | [[_logTextView textStorage] appendAttributedString:logLineAttr]; 310 | } 311 | 312 | @end 313 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2016 Google. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | CFBundleURLTypes 34 | 35 | 36 | CFBundleTypeRole 37 | Editor 38 | CFBundleURLSchemes 39 | 40 | com.googleusercontent.apps.YOUR_CLIENT 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Source/main.m: -------------------------------------------------------------------------------- 1 | /*! @file main.m 2 | @brief Application main entry point. 3 | @copyright 4 | Copyright 2016 Google Inc. 5 | @copydetails 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | #import 20 | 21 | int main(int argc, const char * argv[]) { 22 | return NSApplicationMain(argc, argv); 23 | } 24 | -------------------------------------------------------------------------------- /GTMAppAuth.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = 'GTMAppAuth' 4 | s.version = '5.0.0' 5 | s.swift_version = '4.0' 6 | s.summary = 'Authorize GTM Session Fetcher requests with AppAuth via GTMAppAuth' 7 | 8 | s.description = <<-DESC 9 | 10 | GTMAppAuth enables you to use AppAuth with the Google Toolbox for Mac - Session 11 | Fetcher and Google APIs Client Library for Objective-C For REST libraries by 12 | providing an implementation of GTMFetcherAuthorizationProtocol for authorizing 13 | requests with AppAuth. 14 | 15 | DESC 16 | 17 | s.homepage = 'https://github.com/google/GTMAppAuth' 18 | s.license = { :type => 'Apache', :file => 'LICENSE' } 19 | s.author = 'Google LLC' 20 | 21 | s.source = { :git => 'https://github.com/google/GTMAppAuth.git', :tag => s.version } 22 | s.prefix_header_file = false 23 | s.source_files = 'GTMAppAuth/Sources/**/*.swift' 24 | s.resource_bundles = { 25 | "GTMAppAuth_Privacy" => "GTMAppAuth/Sources/Resources/PrivacyInfo.xcprivacy" 26 | } 27 | 28 | ios_deployment_target = '12.0' 29 | osx_deployment_target = '10.12' 30 | tvos_deployment_target = '10.0' 31 | watchos_deployment_target = '6.0' 32 | s.ios.deployment_target = ios_deployment_target 33 | s.osx.deployment_target = osx_deployment_target 34 | s.tvos.deployment_target = tvos_deployment_target 35 | s.watchos.deployment_target = watchos_deployment_target 36 | 37 | s.framework = 'Security' 38 | s.dependency 'GTMSessionFetcher/Core', '>= 3.3', '< 4.0' 39 | s.dependency 'AppAuth/Core', '~> 2.0' 40 | 41 | s.test_spec 'unit' do |unit_tests| 42 | unit_tests.platforms = { 43 | :ios => ios_deployment_target, 44 | :osx => osx_deployment_target, 45 | :tvos => tvos_deployment_target, 46 | } 47 | unit_tests.source_files = [ 48 | 'GTMAppAuth/Tests/Unit/**/*.swift', 49 | 'GTMAppAuth/Tests/Helpers/**/*.swift', 50 | ] 51 | unit_tests.dependency 'AppAuth/Core' 52 | unit_tests.requires_app_host = true 53 | end 54 | 55 | s.test_spec 'objc-api-integration' do |api_tests| 56 | api_tests.platforms = { 57 | :ios => ios_deployment_target, 58 | :osx => osx_deployment_target, 59 | :tvos => tvos_deployment_target, 60 | } 61 | api_tests.source_files = [ 62 | 'GTMAppAuth/Tests/ObjCIntegration/**/*.m', 63 | 'GTMAppAuth/Tests/Helpers/**/*.swift', 64 | ] 65 | api_tests.dependency 'AppAuth/Core' 66 | api_tests.requires_app_host = true 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /GTMAppAuth/Sources/AuthSessionDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | /// Methods defining the `AuthSession`'s delegate. 20 | @objc(GTMAuthSessionDelegate) 21 | public protocol AuthSessionDelegate { 22 | /// Used to supply additional parameters on token refresh. 23 | /// 24 | /// - Parameters: 25 | /// - authSession: The `AuthSession` needing additional token refresh parameters. 26 | /// - Returns: An optional `[String: String]` supplying the additional token refresh parameters. 27 | @objc optional func additionalTokenRefreshParameters( 28 | forAuthSession authSession: AuthSession 29 | ) -> [String: String]? 30 | 31 | /// A method notifying the delegate that the authorization request failed. 32 | /// 33 | /// Use this method to examine the error behind the failed authorization request and supply a 34 | /// customized error created asynchronously that specifies whatever context is needed. 35 | /// 36 | /// - Parameters: 37 | /// - authSession: The `AuthSession` whose authorization request failed. 38 | /// - originalError: The original `Error` associated with the failure. 39 | /// - completion: An escaping closure to pass back the updated error. 40 | @objc optional func updateError( 41 | forAuthSession authSession: AuthSession, 42 | originalError: Error, 43 | completion: @escaping (Error?) -> Void 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /GTMAppAuth/Sources/AuthSessionStore.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | /// Represents the operations needed to provide `AuthSession` storage. 20 | @objc(GTMAuthSessionStore) 21 | public protocol AuthSessionStore { 22 | /// Saves the provided `AuthSession`. 23 | /// 24 | /// - Parameter authSession: An instance of `AuthSession` to save. 25 | /// - Throws: Any error that may arise during the save. 26 | @objc(saveAuthSession:error:) 27 | func save(authSession: AuthSession) throws 28 | 29 | /// Removes the stored `AuthSession`. 30 | /// 31 | /// - Throws: Any error that may arise during the removal. 32 | @objc(removeAuthSessionWithError:) 33 | func removeAuthSession() throws 34 | 35 | /// Retrieves the stored `AuthSession`. 36 | /// 37 | /// - Throws: Any error that may arise during the retrieval. 38 | @objc(retrieveAuthSessionWithError:) 39 | func retrieveAuthSession() throws -> AuthSession 40 | } 41 | -------------------------------------------------------------------------------- /GTMAppAuth/Sources/KeychainStore/GTMOAuth2Compatibility.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 19 | // the latter doesn't define separate Clang modules for subspecs 20 | #if SWIFT_PACKAGE 21 | import AppAuthCore 22 | import GTMSessionFetcherCore 23 | #else 24 | import AppAuth 25 | import GTMSessionFetcher 26 | #endif 27 | 28 | // Standard OAuth keys 29 | let oauth2AccessTokenKey = "access_token" 30 | let oauth2RefreshTokenKey = "refresh_token" 31 | let oauth2ScopeKey = "scope" 32 | let oauth2ErrorKey = "error" 33 | let oauth2TokenTypeKey = "token_type" 34 | let oauth2ExpiresInKey = "expires_in" 35 | let oauth2CodeKey = "code" 36 | let oauth2AssertionKey = "assertion" 37 | let oauth2RefreshScopeKey = "refreshScope" 38 | 39 | // URI indicating an installed app is signing in. This is described at 40 | // 41 | // https://developers.google.com/identity/protocols/OAuth2InstalledApp#formingtheurl 42 | // 43 | let oobString = "urn:ietf:wg:oauth:2.0:oob" 44 | 45 | /// Class to support serialization and deserialization of `AuthSession` in the format used by 46 | /// GTMOAuth2. 47 | /// 48 | /// The methods of this class are capable of serializing and deserializing auth objects in a way 49 | /// compatible with the serialization in `GTMOAuth2ViewControllerTouch` and 50 | /// `GTMOAuth2WindowController` in GTMOAuth2. 51 | @objc(GTMOAuth2Compatibility) 52 | public final class GTMOAuth2Compatibility: NSObject { 53 | 54 | @available(*, unavailable) 55 | override init() { 56 | super.init() 57 | } 58 | 59 | // MARK: - GTMOAuth2 Utilities 60 | 61 | /// Encodes the given `AuthSession` in a GTMOAuth2 compatible persistence string using URL param 62 | /// key/value encoding. 63 | /// 64 | /// - Parameters: 65 | /// - authSession: The `AuthSession` to serialize in GTMOAuth2 format. 66 | /// - Returns: A `String?` representing the GTMOAuth2 persistence representation of the 67 | /// authorization object. 68 | @objc(persistenceResponseStringForAuthSession:) 69 | public static func persistenceResponseString(forAuthSession authSession: AuthSession) -> String? { 70 | // TODO: (mdmathias) Write a test for this method that ensures nil is returned. 71 | let refreshToken = authSession.authState.refreshToken 72 | let accessToken = authSession.authState.lastTokenResponse?.accessToken 73 | 74 | let dict = [ 75 | oauth2RefreshTokenKey: refreshToken, 76 | oauth2AccessTokenKey: accessToken, 77 | AuthSession.serviceProviderKey: authSession.serviceProvider, 78 | AuthSession.userIDKey: authSession.userID, 79 | AuthSession.userEmailKey: authSession.userEmail, 80 | AuthSession.userEmailIsVerifiedKey: authSession._userEmailIsVerified, 81 | oauth2ScopeKey: authSession.authState.scope 82 | ] 83 | 84 | let responseString = dict 85 | .sorted { $0.key < $1.key } 86 | .compactMap { (key, _) -> String? in 87 | guard let val = dict[key] as? String, 88 | let encodedKey = encodedOAuthValue(forOriginalString: key), 89 | let encodedValue = encodedOAuthValue(forOriginalString: val) else { 90 | return nil 91 | } 92 | return String(format: "%@=%@", arguments: [encodedKey, encodedValue]) 93 | } 94 | .joined(separator: "&") 95 | 96 | return responseString.isEmpty ? nil : responseString 97 | } 98 | 99 | // MARK: - Encoded OAuth Value 100 | 101 | private static func encodedOAuthValue( 102 | forOriginalString originalString: String 103 | ) -> String? { 104 | // For parameters, we'll explicitly leave spaces unescaped now, and replace them with +'s 105 | let forceEscape = "!*'();:@&=+$,/?%#[]" 106 | let escapeCharacters = CharacterSet(charactersIn: forceEscape) 107 | let urlQueryCharacters = CharacterSet.urlQueryAllowed.symmetricDifference(escapeCharacters) 108 | return originalString.addingPercentEncoding(withAllowedCharacters: urlQueryCharacters) 109 | } 110 | 111 | private static var googleAuthorizationURL = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")! 112 | static let googleTokenURL = URL(string: "https://www.googleapis.com/oauth2/v4/token")! 113 | static var googleRevocationURL = URL(string: "https://accounts.google.com/o/oauth2/revoke")! 114 | static var googleUserInfoURL = URL(string: "https://www.googleapis.com/oauth2/v3/userinfo")! 115 | static var nativeClientRedirectURI: String { 116 | return oobString 117 | } 118 | 119 | static func dictionary( 120 | fromKeychainPassword keychainPassword: String 121 | ) -> [String: String] { 122 | let keyValueTuples: [(String, String)] = keychainPassword 123 | .components(separatedBy: "&") 124 | .compactMap { 125 | let equalComps = $0.components(separatedBy: "=") 126 | guard let key = equalComps.first, 127 | let percentsRemovedKey = key.removingPercentEncoding, 128 | let value = equalComps.last, 129 | let percentsRemovedValue = value.removingPercentEncoding else { 130 | return nil 131 | } 132 | return (percentsRemovedKey, percentsRemovedValue) 133 | } 134 | let passwordDictionary = Dictionary(uniqueKeysWithValues: keyValueTuples) 135 | return passwordDictionary 136 | } 137 | 138 | /// Creates an `AuthSession` from the provided persistence string. 139 | /// - Parameters: 140 | /// - persistenceString: The `String` representing the `AuthSession` to create. 141 | /// - tokenURL: The `URL` to use when creating the `AuthSession`. 142 | /// - redirectURI: The `String` URI to use for the `AuthSession`. 143 | /// - clientID: The `String` client ID for the `AuthSession`. 144 | /// - clientSecret: The optional `String` for the `AuthSession`. 145 | /// - Throws: `KeychainStore.Error.failedToConvertRedirectURItoURL` if `redirectURI` cannot be 146 | /// converted to a `URL`. 147 | /// - Returns: An instance of `AuthSession` if successful. 148 | @objc static public func authSession( 149 | forPersistenceString persistenceString: String, 150 | tokenURL: URL, 151 | redirectURI: String, 152 | clientID: String, 153 | clientSecret: String? 154 | ) throws -> AuthSession { 155 | let persistenceDictionary = GTMOAuth2Compatibility.dictionary( 156 | fromKeychainPassword: persistenceString 157 | ) 158 | guard let redirectURL = URL(string: redirectURI) else { 159 | throw KeychainStore.Error.failedToConvertRedirectURItoURL(redirectURI) 160 | } 161 | 162 | let authConfig = OIDServiceConfiguration( 163 | authorizationEndpoint: tokenURL, 164 | tokenEndpoint: tokenURL 165 | ) 166 | 167 | let authRequest = OIDAuthorizationRequest( 168 | configuration: authConfig, 169 | clientId: clientID, 170 | clientSecret: clientSecret, 171 | scope: persistenceDictionary[oauth2ScopeKey], 172 | redirectURL: redirectURL, 173 | responseType: OIDResponseTypeCode, 174 | state: nil, 175 | nonce: nil, 176 | codeVerifier: nil, 177 | codeChallenge: nil, 178 | codeChallengeMethod: nil, 179 | additionalParameters: nil 180 | ) 181 | 182 | let authResponse = OIDAuthorizationResponse( 183 | request: authRequest, 184 | parameters: persistenceDictionary as [String: NSString] 185 | ) 186 | var additionalParameters = persistenceDictionary 187 | additionalParameters.removeValue(forKey: oauth2ScopeKey) 188 | additionalParameters.removeValue(forKey: oauth2RefreshTokenKey) 189 | 190 | let tokenRequest = OIDTokenRequest( 191 | configuration: authConfig, 192 | grantType: "token", 193 | authorizationCode: nil, 194 | redirectURL: redirectURL, 195 | clientID: clientID, 196 | clientSecret: clientSecret, 197 | scope: persistenceDictionary[oauth2ScopeKey], 198 | refreshToken: persistenceDictionary[oauth2RefreshTokenKey], 199 | codeVerifier: nil, 200 | additionalParameters: additionalParameters 201 | ) 202 | let tokenResponse = OIDTokenResponse( 203 | request: tokenRequest, 204 | parameters: persistenceDictionary as [String: NSString] 205 | ) 206 | 207 | let authState = OIDAuthState(authorizationResponse: authResponse, tokenResponse: tokenResponse) 208 | // We're not serializing the token expiry date, so the first refresh needs to be forced. 209 | authState.setNeedsTokenRefresh() 210 | 211 | let authSession = AuthSession( 212 | authState: authState, 213 | serviceProvider: persistenceDictionary[AuthSession.serviceProviderKey], 214 | userID: persistenceDictionary[AuthSession.userIDKey], 215 | userEmail: persistenceDictionary[AuthSession.userEmailKey], 216 | userEmailIsVerified: persistenceDictionary[AuthSession.userEmailIsVerifiedKey] 217 | ) 218 | return authSession 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /GTMAppAuth/Sources/KeychainStore/KeychainAttribute.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | /// The Keychain attribute used to configure the way the keychain stores your items. 20 | @objc(GTMKeychainAttribute) 21 | public final class KeychainAttribute: NSObject { 22 | /// An enumeratiion listing the various attributes used to configure the Keychain. 23 | public enum Attribute { 24 | /// Indicates whether to use the legacy file-based keychain on macOS. 25 | /// 26 | /// This attribute will set `kSecUseDataProtectionKeychain` as `false` in the Keychain query. 27 | case useFileBasedKeychain 28 | /// The `String` name for the access group to use in the Keychain query. 29 | case accessGroup(String) 30 | 31 | /// A `String` representation of the attribute. 32 | public var keyName: String { 33 | switch self { 34 | case .useFileBasedKeychain: 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { 36 | return kSecUseDataProtectionKeychain as String 37 | } else { 38 | fatalError("`kSecUseDataProtectionKeychain is only available on macOS 10.15 and greater") 39 | } 40 | case .accessGroup: 41 | return kSecAttrAccessGroup as String 42 | } 43 | } 44 | } 45 | 46 | /// The set `Attribute` given upon initialization. 47 | public let attribute: Attribute 48 | 49 | /// Creates an instance of `KeychainAttribute`. 50 | /// - Parameters: 51 | /// - attribute: An instance of `KeychainAttribute.Attribute` used to configure Keychain 52 | /// queries. 53 | public init(attribute: Attribute) { 54 | self.attribute = attribute 55 | } 56 | 57 | /// Creates an instance of `KeychainAttribute` whose attribute is set to 58 | /// `.useFileBasedKeychain`. 59 | /// - Returns: An instance of `KeychainAttribute`. 60 | @objc public static let useFileBasedKeychain = KeychainAttribute( 61 | attribute: .useFileBasedKeychain 62 | ) 63 | 64 | /// Creates an instance of `KeychainAttribute` whose attribute is set to `.accessGroup`. 65 | /// - Parameters: 66 | /// - name: The `String` name for the access group. 67 | /// - Returns: An instance of `KeychainAttribute`. 68 | @objc public static func keychainAccessGroup(name: String) -> KeychainAttribute { 69 | return KeychainAttribute(attribute: .accessGroup(name)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /GTMAppAuth/Sources/KeychainStore/KeychainHelper.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | /// A protocol defining the helper API for interacting with the Keychain. 20 | @objc(GTMKeychainHelper) 21 | public protocol KeychainHelper { 22 | var accountName: String { get } 23 | var keychainAttributes: Set { get } 24 | init(keychainAttributes: Set) 25 | func keychainQuery(forService service: String) -> [String: Any] 26 | func password(forService service: String) throws -> String 27 | func passwordData(forService service: String) throws -> Data 28 | func removePassword(forService service: String) throws 29 | func setPassword(_ password: String, forService service: String) throws 30 | func setPassword(_ password: String, forService service: String, accessibility: CFTypeRef) throws 31 | func setPassword(data: Data, forService service: String, accessibility: CFTypeRef?) throws 32 | } 33 | 34 | /// An internally scoped keychain helper. 35 | final class KeychainWrapper: KeychainHelper { 36 | let accountName = "OAuth" 37 | let keychainAttributes: Set 38 | 39 | init(keychainAttributes: Set = []) { 40 | self.keychainAttributes = keychainAttributes 41 | } 42 | 43 | func keychainQuery(forService service: String) -> [String: Any] { 44 | var query: [String: Any] = [ 45 | kSecClass as String: kSecClassGenericPassword, 46 | kSecAttrAccount as String : accountName, 47 | kSecAttrService as String: service, 48 | ] 49 | 50 | if #available(macOS 10.15, macCatalyst 13.1, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { 51 | query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue 52 | } 53 | 54 | keychainAttributes.forEach { configuration in 55 | switch configuration.attribute { 56 | case .useFileBasedKeychain: 57 | if #available(macOS 10.15, macCatalyst 13.1, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { 58 | query[configuration.attribute.keyName] = kCFBooleanFalse 59 | } 60 | case .accessGroup(let name): 61 | query[configuration.attribute.keyName] = name 62 | } 63 | } 64 | 65 | return query 66 | } 67 | 68 | func password(forService service: String) throws -> String { 69 | let passwordData = try passwordData(forService: service) 70 | guard let result = String(data: passwordData, encoding: .utf8) else { 71 | throw KeychainStore.Error.unexpectedPasswordData(forItemName: service) 72 | } 73 | return result 74 | } 75 | 76 | func passwordData(forService service: String) throws -> Data { 77 | guard !service.isEmpty else { throw KeychainStore.Error.noService } 78 | 79 | var passwordItem: AnyObject? 80 | var keychainQuery = keychainQuery(forService: service) 81 | keychainQuery[kSecReturnData as String] = true 82 | keychainQuery[kSecMatchLimit as String] = kSecMatchLimitOne 83 | let status = SecItemCopyMatching(keychainQuery as CFDictionary, &passwordItem) 84 | 85 | guard status != errSecItemNotFound else { 86 | throw KeychainStore.Error.passwordNotFound(forItemName: service) 87 | } 88 | 89 | guard status == errSecSuccess else { throw KeychainStore.Error.unhandled(status: status) } 90 | 91 | guard let result = passwordItem as? Data else { 92 | throw KeychainStore.Error.unexpectedPasswordData(forItemName: service) 93 | } 94 | 95 | return result 96 | } 97 | 98 | func removePassword(forService service: String) throws { 99 | guard !service.isEmpty else { throw KeychainStore.Error.noService } 100 | let keychainQuery = keychainQuery(forService: service) 101 | let status = SecItemDelete(keychainQuery as CFDictionary) 102 | 103 | guard status != errSecItemNotFound else { 104 | throw KeychainStore.Error.failedToDeletePasswordBecauseItemNotFound(itemName: service) 105 | } 106 | guard status == noErr else { 107 | throw KeychainStore.Error.failedToDeletePassword(forItemName: service) 108 | } 109 | } 110 | 111 | func setPassword(_ password: String, forService service: String) throws { 112 | let passwordData = Data(password.utf8) 113 | try setPassword(data: passwordData, forService: service, accessibility: nil) 114 | } 115 | 116 | func setPassword( 117 | _ password: String, 118 | forService service: String, 119 | accessibility: CFTypeRef 120 | ) throws { 121 | let passwordData = Data(password.utf8) 122 | try setPassword(data: passwordData, forService: service, accessibility: accessibility) 123 | } 124 | 125 | func setPassword(data: Data, forService service: String, accessibility: CFTypeRef?) throws { 126 | guard !service.isEmpty else { throw KeychainStore.Error.noService } 127 | do { 128 | try removePassword(forService: service) 129 | } catch KeychainStore.Error.failedToDeletePasswordBecauseItemNotFound { 130 | // Don't throw; password doesn't exist since the password is being saved for the first time 131 | } catch { 132 | // throw here since this is some other error 133 | throw error 134 | } 135 | guard !data.isEmpty else { return } 136 | var keychainQuery = keychainQuery(forService: service) 137 | keychainQuery[kSecValueData as String] = data 138 | 139 | if let accessibility = accessibility { 140 | keychainQuery[kSecAttrAccessible as String] = accessibility 141 | } 142 | 143 | let status = SecItemAdd(keychainQuery as CFDictionary, nil) 144 | guard status == noErr else { 145 | throw KeychainStore.Error.failedToSetPassword(forItemName: service) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /GTMAppAuth/Sources/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyTracking 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/AuthorizationTestingHelp.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 19 | // the latter doesn't define separate Clang modules for subspecs 20 | #if SWIFT_PACKAGE 21 | import AppAuthCore 22 | #else 23 | import AppAuth 24 | #endif 25 | import XCTest 26 | import GTMAppAuth 27 | 28 | /// The delegate object passed to selector-based callback to authorize requests. 29 | @objc(GTMAuthorizationTestDelegate) 30 | public class AuthorizationTestDelegate: NSObject { 31 | /// The authorization passed back to this delegate. 32 | @objc public var passedAuthorization: AuthSession? 33 | /// The request passed back to this delegate. 34 | @objc public var passedRequest: NSMutableURLRequest? 35 | /// The error passed back to this delegate. 36 | @objc public var passedError: NSError? 37 | /// The expectation needing fulfillment when this delegate receives its callback. 38 | @objc public let expectation: XCTestExpectation 39 | 40 | /// An initializer creating the delegate. 41 | /// 42 | /// - Parameter expectation: The `XCTestExpectation` to be fulfilled. 43 | @objc public init(expectation: XCTestExpectation) { 44 | self.expectation = expectation 45 | } 46 | 47 | /// The function serving as the callback for this delegate. 48 | /// 49 | /// - Parameters: 50 | /// - authSession: The `AuthSession` authorizing the request. 51 | /// - request: The request to be authorized. 52 | /// - error: The `NSError?` should one arise during authorization. 53 | @objc public func authentication( 54 | _ authSession: AuthSession, 55 | request: NSMutableURLRequest, 56 | finishedWithError error: NSError? 57 | ) { 58 | passedAuthorization = authSession 59 | passedRequest = request 60 | passedError = error 61 | 62 | expectation.fulfill() 63 | } 64 | } 65 | 66 | /// A testing helper that does not implement the update error method in `AuthSessionDelegate`. 67 | @objc(GTMAuthSessionDelegateProvideMissingEMMErrorHandling) 68 | public class AuthSessionDelegateProvideMissingEMMErrorHandling: NSObject, AuthSessionDelegate { 69 | /// The `AuthSession` to which this delegate was given. 70 | @objc public var originalAuthSession: AuthSession? 71 | 72 | /// Whether or not the delegate callback for additional refresh parameters was called. 73 | /// 74 | /// - Note: Defaults to `false`. 75 | @objc public var additionalRefreshParametersCalled = false 76 | 77 | /// Whether or not the delegate callback for authorization request failure was called. 78 | /// 79 | /// - Note: Defaults to `false`. 80 | @objc public var updatedErrorCalled = false 81 | 82 | /// The expected error from the delegate callback. 83 | @objc public let expectedError: NSError? 84 | 85 | @objc public init(originalAuthSession: AuthSession, expectedError: NSError? = nil) { 86 | self.originalAuthSession = originalAuthSession 87 | self.expectedError = expectedError 88 | } 89 | 90 | public func additionalTokenRefreshParameters( 91 | forAuthSession authSession: AuthSession 92 | ) -> [String : String]? { 93 | XCTAssertEqual(authSession, originalAuthSession) 94 | additionalRefreshParametersCalled = true 95 | return [:] 96 | } 97 | } 98 | 99 | /// A testing helper given to `AuthSession`'s `delegate` to verify delegate callbacks. 100 | @objc(GTMAuthSessionDelegateProvider) 101 | public class AuthSessionDelegateProvider: NSObject, AuthSessionDelegate { 102 | /// The `AuthSession` to which this delegate was given. 103 | @objc public var originalAuthSession: AuthSession? 104 | 105 | /// Whether or not the delegate callback for additional refresh parameters was called. 106 | /// 107 | /// - Note: Defaults to `false`. 108 | @objc public var additionalRefreshParametersCalled = false 109 | 110 | /// Whether or not the delegate callback for authorization request failure was called. 111 | /// 112 | /// - Note: Defaults to `false`. 113 | @objc public var updatedErrorCalled = false 114 | 115 | /// The expected error from the delegate callback. 116 | @objc public let expectedError: NSError? 117 | 118 | @objc public init(originalAuthSession: AuthSession, expectedError: NSError? = nil) { 119 | self.originalAuthSession = originalAuthSession 120 | self.expectedError = expectedError 121 | } 122 | 123 | public func additionalTokenRefreshParameters( 124 | forAuthSession authSession: AuthSession 125 | ) -> [String : String]? { 126 | XCTAssertEqual(authSession, originalAuthSession) 127 | additionalRefreshParametersCalled = true 128 | return [:] 129 | } 130 | 131 | public func updateError( 132 | forAuthSession authSession: AuthSession, 133 | originalError: Error, 134 | completion: @escaping (Error?) -> Void 135 | ) { 136 | XCTAssertEqual(authSession, originalAuthSession) 137 | updatedErrorCalled = true 138 | completion(expectedError) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/GTMAppAuthTestingHelpers.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | /// A protocol for creating a test instance of `Self`. 20 | @objc(GTMTesting) 21 | public protocol Testing { 22 | static func testInstance() -> Self 23 | } 24 | 25 | // MARK: - Constants 26 | 27 | @objc(GTMTestingConstants) 28 | public class TestingConstants: NSObject { 29 | @objc public static let testAccessGroup = "testAccessGroup" 30 | @objc public static let testAccessToken = "access_token" 31 | @objc public static let accessTokenExpiresIn = 3600 32 | @objc public static let testRefreshToken = "refresh_token" 33 | @objc public static let serverAuthCode = "server_auth_code" 34 | 35 | @objc public static let alg = "RS256" 36 | @objc public static let kid = "alkjdfas" 37 | @objc public static let typ = "JWT" 38 | @objc public static let userID = "123456789" 39 | @objc public static let hostedDomain = "fakehosteddomain.com" 40 | @objc public static let issuer = "https://test.com" 41 | @objc public static let audience = "audience" 42 | @objc public static let IDTokenExpires = 1000 43 | @objc public static let issuedAt = 0 44 | 45 | @objc public static let fatNameKey = "name"; 46 | @objc public static let fatGivenNameKey = "given_name" 47 | @objc public static let fatFamilyNameKey = "family_name" 48 | @objc public static let fatPictureURLKey = "picture" 49 | 50 | @objc public static let fatName = "fake username" 51 | @objc public static let fatGivenName = "fake" 52 | @objc public static let fatFamilyName = "username" 53 | @objc public static let fatPictureURL = "fake_user_picture_url" 54 | 55 | @objc public static let testClientID = "87654321.googleusercontent.com" 56 | @objc public static let testScope1 = "email" 57 | @objc public static let testScope2 = "profile" 58 | @objc public static let testCodeVerifier = "codeVerifier" 59 | @objc public static let testPassword = "foo" 60 | @objc public static let testKeychainItemName = "testName" 61 | @objc public static let testServiceProvider = "fooProvider" 62 | @objc public static let testUserID = "123456789" 63 | @objc public static let testEmail = "foo@foo.com" 64 | @objc public static let testClientSecret = "fooSecret" 65 | @objc public static let testTokenURL: URL = URL(string: "https://testTokenURL.com")! 66 | @objc public static let testRedirectURI = "https://testRedirectURI.com" 67 | } 68 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/KeychainHelperFake.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import GTMAppAuth 19 | 20 | @objc(GTMKeychainHelperFake) 21 | public class KeychainHelperFake: NSObject, KeychainHelper { 22 | @objc public var useFileBasedKeychain = false 23 | @objc public var passwordStore = [String: Data]() 24 | @objc public let accountName = "OauthTest" 25 | @objc public let keychainAttributes: Set 26 | @objc public var generatedKeychainQuery: [String: Any]? 27 | /// Data generated from `NSKeyedArchiver` treated as a property list to test class name mapping. 28 | private(set) var archiveDataPropertyList: Any? 29 | /// Data generated from `NSKeyedUnarchiver` treated as a property list to test class name mapping. 30 | private(set) var unarchiveDataPropertyList: Any? 31 | 32 | @objc public required init(keychainAttributes: Set) { 33 | self.keychainAttributes = keychainAttributes 34 | } 35 | 36 | @objc public func keychainQuery(forService service: String) -> [String : Any] { 37 | var query: [String: Any] = [ 38 | kSecClass as String: kSecClassGenericPassword, 39 | kSecAttrAccount as String : accountName, 40 | kSecAttrService as String: service, 41 | ] 42 | 43 | if #available(macOS 10.15, macCatalyst 13.1, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { 44 | query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue 45 | } 46 | 47 | keychainAttributes.forEach { configuration in 48 | switch configuration.attribute { 49 | case .useFileBasedKeychain: 50 | query[configuration.attribute.keyName] = kCFBooleanFalse 51 | case .accessGroup(let name): 52 | query[configuration.attribute.keyName] = name 53 | } 54 | } 55 | 56 | return query 57 | } 58 | 59 | @objc public func password(forService service: String) throws -> String { 60 | guard !service.isEmpty else { throw KeychainStore.Error.noService } 61 | 62 | let passwordData = try passwordData(forService: service) 63 | guard let password = String(data: passwordData, encoding: .utf8) else { 64 | throw KeychainStore.Error.passwordNotFound(forItemName: service) 65 | } 66 | return password 67 | } 68 | 69 | @objc public func passwordData(forService service: String) throws -> Data { 70 | guard !service.isEmpty else { throw KeychainStore.Error.noService } 71 | 72 | generatedKeychainQuery = keychainQuery(forService: service) 73 | guard let passwordData = passwordStore[service + accountName] else { 74 | throw KeychainStore.Error.passwordNotFound(forItemName: service) 75 | } 76 | 77 | // Use `try?` instead of try to avoid throwing in the ObjC integration tests and test failures 78 | self.unarchiveDataPropertyList = try? PropertyListSerialization.propertyList( 79 | from: passwordData, 80 | format: nil 81 | ) 82 | 83 | return passwordData 84 | } 85 | 86 | @objc public func removePassword(forService service: String) throws { 87 | guard !service.isEmpty else { throw KeychainStore.Error.noService } 88 | 89 | generatedKeychainQuery = keychainQuery(forService: service) 90 | guard let _ = passwordStore.removeValue(forKey: service + accountName) else { 91 | throw KeychainStore.Error.failedToDeletePasswordBecauseItemNotFound(itemName: service) 92 | } 93 | } 94 | 95 | @objc public func setPassword(_ password: String, forService service: String) throws { 96 | do { 97 | try removePassword(forService: service) 98 | } catch KeychainStore.Error.failedToDeletePasswordBecauseItemNotFound { 99 | // No need to throw this error since we are setting a new password 100 | } catch { 101 | throw error 102 | } 103 | 104 | guard let passwordData = password.data(using: .utf8) else { 105 | throw KeychainStore.Error.unexpectedPasswordData(forItemName: service) 106 | } 107 | try setPassword(data: passwordData, forService: service, accessibility: nil) 108 | } 109 | 110 | @objc public func setPassword( 111 | _ password: String, 112 | forService service: String, 113 | accessibility: CFTypeRef 114 | ) throws { 115 | do { 116 | try removePassword(forService: service) 117 | } catch KeychainStore.Error.failedToDeletePasswordBecauseItemNotFound { 118 | // No need to throw this error since we are setting a new password 119 | } catch { 120 | throw error 121 | } 122 | 123 | guard let passwordData = password.data(using: .utf8) else { 124 | throw KeychainStore.Error.unexpectedPasswordData(forItemName: service) 125 | } 126 | try setPassword(data: passwordData, forService: service, accessibility: accessibility) 127 | } 128 | 129 | @objc public func setPassword( 130 | data: Data, 131 | forService service: String, 132 | accessibility: CFTypeRef? 133 | ) throws { 134 | guard !service.isEmpty else { throw KeychainStore.Error.noService } 135 | 136 | // Use `try?` instead of try to avoid throwing in the ObjC integration tests and test failures 137 | self.archiveDataPropertyList = try? PropertyListSerialization.propertyList( 138 | from: data, 139 | format: nil 140 | ) 141 | generatedKeychainQuery = keychainQuery(forService: service) 142 | passwordStore.updateValue(data, forKey: service + accountName) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/OIDAuthStateTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 18 | // the latter doesn't define separate Clang modules for subspecs 19 | #if SWIFT_PACKAGE 20 | import AppAuthCore 21 | #else 22 | import AppAuth 23 | #endif 24 | 25 | /// A protocol to help create test instances of `OIDAuthState` with various 26 | /// arguments. 27 | @objc public protocol AuthStateTesting: Testing { 28 | static func testInstance(idToken: String) -> Self 29 | static func testInstance(tokenResponse: OIDTokenResponse) -> Self 30 | static func testInstance( 31 | authorizationResponse: OIDAuthorizationResponse?, 32 | tokenResponse: OIDTokenResponse?, 33 | registrationResponse: OIDRegistrationResponse? 34 | ) -> Self 35 | } 36 | 37 | @objc extension OIDAuthState: AuthStateTesting { 38 | public static func testInstance() -> Self { 39 | return OIDAuthState( 40 | authorizationResponse: OIDAuthorizationResponse.testInstance(), 41 | tokenResponse: OIDTokenResponse.testInstance() 42 | ) as! Self 43 | } 44 | 45 | public static func testInstance(idToken: String) -> Self { 46 | return testInstance( 47 | tokenResponse: OIDTokenResponse.testInstance(idToken: idToken) 48 | ) 49 | } 50 | 51 | public static func testInstance(tokenResponse: OIDTokenResponse) -> Self { 52 | return testInstance( 53 | authorizationResponse: OIDAuthorizationResponse.testInstance(), 54 | tokenResponse: tokenResponse, 55 | registrationResponse: nil 56 | ) 57 | } 58 | 59 | public static func testInstance( 60 | authorizationResponse: OIDAuthorizationResponse?, 61 | tokenResponse: OIDTokenResponse?, 62 | registrationResponse: OIDRegistrationResponse? 63 | ) -> Self { 64 | return OIDAuthState( 65 | authorizationResponse: authorizationResponse, 66 | tokenResponse: tokenResponse, 67 | registrationResponse: registrationResponse 68 | ) as! Self 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/OIDAuthorizationRequestTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 18 | // the latter doesn't define separate Clang modules for subspecs 19 | #if SWIFT_PACKAGE 20 | import AppAuthCore 21 | #else 22 | import AppAuth 23 | #endif 24 | 25 | @objc extension OIDAuthorizationRequest: Testing { 26 | public static func testInstance() -> Self { 27 | return OIDAuthorizationRequest( 28 | configuration: OIDServiceConfiguration.testInstance(), 29 | clientId: TestingConstants.testClientID, 30 | scopes: [TestingConstants.testScope1, TestingConstants.testScope2], 31 | redirectURL: URL(string: "http://test.com")!, 32 | responseType: OIDResponseTypeCode, 33 | additionalParameters: nil 34 | ) as! Self 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/OIDAuthorizationResponseTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 18 | // the latter doesn't define separate Clang modules for subspecs 19 | #if SWIFT_PACKAGE 20 | import AppAuthCore 21 | #else 22 | import AppAuth 23 | #endif 24 | 25 | /// Create a testing instance of `OIDAuthorizationResponse` taking arguments for additional 26 | /// parameters and an error description. 27 | @objc public protocol AuthorizationResponseTesting: Testing { 28 | static func testInstance( 29 | additionalParameters: [String: String]?, 30 | errorDescription: String? 31 | ) -> Self 32 | } 33 | 34 | @objc extension OIDAuthorizationResponse: AuthorizationResponseTesting { 35 | @objc public static func testInstance() -> Self { 36 | return testInstance( 37 | additionalParameters: nil, 38 | errorDescription: nil 39 | ) 40 | } 41 | 42 | @objc public static func testInstance( 43 | additionalParameters: [String : String]?, 44 | errorDescription: String? 45 | ) -> Self { 46 | var parameters = [String: String]() 47 | if let errorString = errorDescription { 48 | parameters["error"] = errorString 49 | } else { 50 | parameters["code"] = "authorization_code" 51 | parameters["state"] = "state" 52 | parameters["token_type"] = OIDGrantTypeAuthorizationCode 53 | if let additionalParameters = additionalParameters { 54 | parameters.merge(additionalParameters) { _, new in new } 55 | } 56 | } 57 | return OIDAuthorizationResponse( 58 | request: OIDAuthorizationRequest.testInstance(), 59 | parameters: parameters as [String: NSCopying & NSObjectProtocol] 60 | ) as! Self 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/OIDRegistrationRequestTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 18 | // the latter doesn't define separate Clang modules for subspecs 19 | #if SWIFT_PACKAGE 20 | import AppAuthCore 21 | #else 22 | import AppAuth 23 | #endif 24 | 25 | /// Protocol for creating a test instance of `OIDRegistrationRequest` with a configuration. 26 | @objc public protocol RegistrationRequestTesting: Testing { 27 | static func testInstance(configuration: OIDServiceConfiguration) -> Self 28 | } 29 | 30 | @objc extension OIDRegistrationRequest: RegistrationRequestTesting { 31 | public static func testInstance() -> Self { 32 | testInstance(configuration: OIDServiceConfiguration.testInstance()) 33 | } 34 | 35 | public static func testInstance(configuration: OIDServiceConfiguration) -> Self { 36 | return OIDRegistrationRequest( 37 | configuration: configuration, 38 | redirectURIs: [], 39 | responseTypes: nil, 40 | grantTypes: nil, 41 | subjectType: nil, 42 | tokenEndpointAuthMethod: nil, 43 | additionalParameters: nil 44 | ) as! Self 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/OIDRegistrationResponseTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 18 | // the latter doesn't define separate Clang modules for subspecs 19 | #if SWIFT_PACKAGE 20 | import AppAuthCore 21 | #else 22 | import AppAuth 23 | #endif 24 | 25 | 26 | /// Protocol for creating a test instance of `OIDRegistrationResponse` with a request and 27 | /// parameters. 28 | @objc public protocol RegistrationResponseTesting: Testing { 29 | static func testInstance( 30 | request: OIDRegistrationRequest, 31 | parameters: [String: String] 32 | ) -> Self 33 | } 34 | 35 | @objc extension OIDRegistrationResponse: RegistrationResponseTesting { 36 | public static func testInstance() -> Self { 37 | return testInstance( 38 | request: OIDRegistrationRequest.testInstance(), 39 | parameters: [:] 40 | ) 41 | } 42 | 43 | public static func testInstance( 44 | request: OIDRegistrationRequest, 45 | parameters: [String: String] 46 | ) -> Self { 47 | return OIDRegistrationResponse( 48 | request: request, 49 | parameters: parameters as [String: NSCopying & NSObjectProtocol] 50 | ) as! Self 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/OIDServiceConfigurationTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 18 | // the latter doesn't define separate Clang modules for subspecs 19 | #if SWIFT_PACKAGE 20 | import AppAuthCore 21 | #else 22 | import AppAuth 23 | #endif 24 | 25 | /// Extension for creating a test instance of `OIDServiceConfiguration`. 26 | @objc extension OIDServiceConfiguration: Testing { 27 | public static func testInstance() -> Self { 28 | return OIDServiceConfiguration( 29 | authorizationEndpoint: URL(string: "https://test.com/auth")!, 30 | tokenEndpoint: URL(string: "https://test.com/token")! 31 | ) as! Self 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/OIDTokenRequestTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 18 | // the latter doesn't define separate Clang modules for subspecs 19 | #if SWIFT_PACKAGE 20 | import AppAuthCore 21 | #else 22 | import AppAuth 23 | #endif 24 | 25 | /// Protocol for creating a test instance of `OIDTokenRequest` with additional parameters. 26 | @objc public protocol TokenRequestTesting: Testing { 27 | static func testInstance(additionalParameters: [String: String]?) -> Self 28 | } 29 | 30 | @objc extension OIDTokenRequest: TokenRequestTesting { 31 | public static func testInstance() -> Self { 32 | testInstance(additionalParameters: nil) 33 | } 34 | 35 | public static func testInstance(additionalParameters: [String : String]?) -> Self { 36 | let authorizationResponse = OIDAuthorizationResponse.testInstance() 37 | let authorizationRequest = authorizationResponse.request 38 | let scopes = OIDScopeUtilities.scopesArray( 39 | with: authorizationRequest.scope ?? "" 40 | ) 41 | return OIDTokenRequest( 42 | configuration: authorizationResponse.request.configuration, 43 | grantType: OIDGrantTypeAuthorizationCode, 44 | authorizationCode: authorizationResponse.authorizationCode, 45 | redirectURL: authorizationRequest.redirectURL, 46 | clientID: authorizationRequest.clientID, 47 | clientSecret: authorizationRequest.clientID, 48 | scopes: scopes, 49 | refreshToken: "refreshToken", 50 | codeVerifier: authorizationRequest.codeVerifier, 51 | additionalParameters: additionalParameters 52 | ) as! Self 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Helpers/OIDTokenResponseTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 18 | // the latter doesn't define separate Clang modules for subspecs 19 | #if SWIFT_PACKAGE 20 | import AppAuthCore 21 | #else 22 | import AppAuth 23 | #endif 24 | 25 | /// Protocol for creating a test instance of `OIDTokenResponse`. 26 | @objc public protocol TokenResponseTesting: Testing { 27 | static func testInstance(idToken: String) -> Self 28 | static func testInstance( 29 | idToken: String, 30 | accessToken: String?, 31 | expires: NSNumber?, 32 | tokenRequest: OIDTokenRequest? 33 | ) -> Self 34 | } 35 | 36 | @objc extension OIDTokenResponse: TokenResponseTesting { 37 | public static func testInstance() -> Self { 38 | return testInstance(idToken: idToken) 39 | } 40 | 41 | public static func testInstance(idToken: String) -> Self { 42 | return OIDTokenResponse.testInstance( 43 | idToken: idToken, 44 | accessToken: nil, 45 | expires: nil, 46 | tokenRequest: nil 47 | ) as! Self 48 | } 49 | 50 | public static func testInstanceWithoutAccessToken( 51 | idToken: String, 52 | expires: NSNumber?, 53 | tokenRequest: OIDTokenRequest? 54 | ) -> Self { 55 | let parameters: [String: NSObject & NSCopying] = [ 56 | "expires_in": (expires ?? NSNumber(value: TestingConstants.accessTokenExpiresIn)), 57 | "token_type": "example_token_type" as NSString, 58 | "refresh_token": TestingConstants.testRefreshToken as NSString, 59 | "scope": OIDScopeUtilities.scopes(with: [TestingConstants.testScope2]) as NSString, 60 | "server_code": TestingConstants.serverAuthCode as NSString, 61 | "id_token": idToken as NSString 62 | ] 63 | 64 | return OIDTokenResponse( 65 | request: tokenRequest ?? OIDTokenRequest.testInstance(), 66 | parameters: parameters 67 | ) as! Self 68 | } 69 | 70 | public static func testInstanceWithEmptyAccessToken( 71 | idToken: String, 72 | expires: NSNumber?, 73 | tokenRequest: OIDTokenRequest? 74 | ) -> Self { 75 | let parameters: [String: NSObject & NSCopying] = [ 76 | "access_token": "" as NSString, 77 | "expires_in": (expires ?? NSNumber(value: TestingConstants.accessTokenExpiresIn)), 78 | "token_type": "example_token_type" as NSString, 79 | "refresh_token": TestingConstants.testRefreshToken as NSString, 80 | "scope": OIDScopeUtilities.scopes(with: [TestingConstants.testScope2]) as NSString, 81 | "server_code": TestingConstants.serverAuthCode as NSString, 82 | "id_token": idToken as NSString 83 | ] 84 | 85 | return OIDTokenResponse( 86 | request: tokenRequest ?? OIDTokenRequest.testInstance(), 87 | parameters: parameters 88 | ) as! Self 89 | } 90 | 91 | public static func testInstance( 92 | idToken: String, 93 | accessToken: String?, 94 | expires: NSNumber?, 95 | tokenRequest: OIDTokenRequest? 96 | ) -> Self { 97 | let parameters: [String: NSObject & NSCopying] = [ 98 | "access_token": (accessToken ?? TestingConstants.testAccessToken) as NSString, 99 | "expires_in": (expires ?? NSNumber(value: TestingConstants.accessTokenExpiresIn)), 100 | "token_type": "example_token_type" as NSString, 101 | "refresh_token": TestingConstants.testRefreshToken as NSString, 102 | "scope": OIDScopeUtilities.scopes(with: [TestingConstants.testScope2]) as NSString, 103 | "server_code": TestingConstants.serverAuthCode as NSString, 104 | "id_token": idToken as NSString 105 | ] 106 | 107 | return OIDTokenResponse( 108 | request: tokenRequest ?? OIDTokenRequest.testInstance(), 109 | parameters: parameters 110 | ) as! Self 111 | } 112 | 113 | static var idToken: String { 114 | return idToken(sub: TestingConstants.userID, exp: TestingConstants.IDTokenExpires, fat: false) 115 | } 116 | 117 | static func idToken(sub: String, exp: Int, fat: Bool) -> String { 118 | let headerContents = [ 119 | "alg": TestingConstants.alg, 120 | "kid": TestingConstants.kid, 121 | "typ": TestingConstants.typ, 122 | ] 123 | 124 | // `try!` is fine here since failing is okay in the test 125 | let headerJson = try! JSONSerialization.data( 126 | withJSONObject: headerContents, 127 | options: .prettyPrinted 128 | ) 129 | 130 | var payloadContents = [ 131 | "sub": sub, 132 | "hd": TestingConstants.hostedDomain, 133 | "iss": TestingConstants.issuer, 134 | "aud": TestingConstants.audience, 135 | "exp": exp, 136 | "iat": TestingConstants.issuedAt 137 | ] as [String : Any] 138 | 139 | if fat { 140 | payloadContents[TestingConstants.fatNameKey] = TestingConstants.fatName 141 | payloadContents[TestingConstants.fatGivenNameKey] = TestingConstants.fatGivenName 142 | payloadContents[TestingConstants.fatFamilyNameKey] = TestingConstants.fatFamilyName 143 | payloadContents[TestingConstants.fatPictureURLKey] = TestingConstants.fatPictureURL 144 | } 145 | 146 | let payloadData = try! JSONSerialization.data( 147 | withJSONObject: payloadContents, 148 | options: .prettyPrinted 149 | ) 150 | 151 | return "\(headerJson.base64EncodedString()).\(payloadData.base64EncodedString()).FakeSignature" 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/ObjCIntegration/GTMAuthSessionTests.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | #if SWIFT_PACKAGE 20 | @import AppAuthCore; 21 | @import TestHelpers; 22 | #else 23 | @import AppAuth; 24 | #import "GTMAppAuth_Unit_objc_api_integration-Swift.h" 25 | #endif 26 | @import GTMAppAuth; 27 | 28 | @interface GTMAuthSessionTests : XCTestCase 29 | 30 | @property (nonatomic) NSURL *googleAuthzEndpoint; 31 | @property (nonatomic) NSURL *tokenEndpoint; 32 | @property (nonatomic) NSURL *secureURL; 33 | @property (nonatomic) NSURL *insecureURL; 34 | @property (nonatomic) NSTimeInterval expectationTimeout; 35 | 36 | @end 37 | 38 | @implementation GTMAuthSessionTests 39 | 40 | - (void)setUp { 41 | self.secureURL = [NSURL URLWithString:@"https://fake.com"]; 42 | self.insecureURL = [NSURL URLWithString:@"http://fake.com"]; 43 | self.googleAuthzEndpoint = [NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"]; 44 | self.tokenEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"]; 45 | self.expectationTimeout = 5; 46 | [super setUp]; 47 | } 48 | 49 | - (void)testInitWithOIDAuthState { 50 | GTMAuthSession *authSession = 51 | [[GTMAuthSession alloc] initWithAuthState:[OIDAuthState testInstance]]; 52 | XCTAssertNotNil(authSession); 53 | } 54 | 55 | - (void)testDesignatedInitializer { 56 | GTMAuthSession *authSession = 57 | [[GTMAuthSession alloc] initWithAuthState:OIDAuthState.testInstance 58 | serviceProvider:GTMTestingConstants.testServiceProvider 59 | userID:GTMTestingConstants.testUserID 60 | userEmail:GTMTestingConstants.testEmail 61 | userEmailIsVerified:@"y"]; 62 | XCTAssertNotNil(authSession); 63 | XCTAssertTrue(authSession.authState.isAuthorized); 64 | XCTAssertEqualObjects(authSession.serviceProvider, [GTMTestingConstants testServiceProvider]); 65 | XCTAssertEqualObjects(authSession.userID, [GTMTestingConstants testUserID]); 66 | XCTAssertEqualObjects(authSession.userEmail, [GTMTestingConstants testEmail]); 67 | XCTAssertTrue(authSession.userEmailIsVerified); 68 | } 69 | 70 | - (void)testAuthorizeSecureRequestWithCompletion { 71 | XCTestExpectation *authRequestExpectation = 72 | [[XCTestExpectation alloc] initWithDescription:@"Authorize with completion"]; 73 | 74 | GTMAuthSession *authSession = 75 | [[GTMAuthSession alloc] initWithAuthState:OIDAuthState.testInstance]; 76 | NSMutableURLRequest *secureRequest = [NSMutableURLRequest requestWithURL:self.secureURL]; 77 | 78 | [authSession authorizeRequest:secureRequest completionHandler:^(NSError * _Nullable error) { 79 | XCTAssertNil(error); 80 | [authRequestExpectation fulfill]; 81 | }]; 82 | 83 | XCTAssertTrue([authSession isAuthorizingRequest:secureRequest]); 84 | [self waitForExpectations:@[authRequestExpectation] timeout:self.expectationTimeout]; 85 | XCTAssertTrue([authSession isAuthorizedRequest:secureRequest]); 86 | } 87 | 88 | - (void)testAuthorizeSecureRequestWithCompletionEmptyAccessTokenError { 89 | XCTestExpectation *authRequestExpectation = 90 | [[XCTestExpectation alloc] initWithDescription:@"Authorize with completion"]; 91 | 92 | OIDTokenResponse *tokenResponse = 93 | [OIDTokenResponse testInstanceWithEmptyAccessTokenWithIdToken:@"" 94 | expires:nil 95 | tokenRequest:nil]; 96 | GTMAuthSession *authSession = 97 | [[GTMAuthSession alloc] initWithAuthState:[OIDAuthState testInstanceWithTokenResponse:tokenResponse]]; 98 | NSMutableURLRequest *secureRequest = [NSMutableURLRequest requestWithURL:self.secureURL]; 99 | 100 | [authSession authorizeRequest:secureRequest completionHandler:^(NSError * _Nullable error) { 101 | XCTAssertNotNil(error); 102 | XCTAssertEqual(error.code, GTMAuthSessionErrorCodeAccessTokenEmptyForRequest); 103 | [authRequestExpectation fulfill]; 104 | }]; 105 | 106 | XCTAssertTrue([authSession isAuthorizingRequest:secureRequest]); 107 | [self waitForExpectations:@[authRequestExpectation] timeout:self.expectationTimeout]; 108 | XCTAssertFalse([authSession isAuthorizedRequest:secureRequest]); 109 | } 110 | 111 | - (void)testAuthorizeSecureRequestWithCompletionNilAccessTokenError { 112 | XCTestExpectation *authRequestExpectation = 113 | [[XCTestExpectation alloc] initWithDescription:@"Authorize with completion"]; 114 | 115 | OIDTokenResponse *tokenResponse = 116 | [OIDTokenResponse testInstanceWithoutAccessTokenWithIdToken:@"" 117 | expires:nil 118 | tokenRequest:nil]; 119 | 120 | GTMAuthSession *authSession = 121 | [[GTMAuthSession alloc] initWithAuthState:[OIDAuthState testInstanceWithTokenResponse:tokenResponse]]; 122 | NSMutableURLRequest *secureRequest = [NSMutableURLRequest requestWithURL:self.secureURL]; 123 | 124 | [authSession authorizeRequest:secureRequest completionHandler:^(NSError * _Nullable error) { 125 | XCTAssertNotNil(error); 126 | XCTAssertEqual(error.code, GTMAuthSessionErrorCodeAccessTokenEmptyForRequest); 127 | [authRequestExpectation fulfill]; 128 | }]; 129 | 130 | XCTAssertTrue([authSession isAuthorizingRequest:secureRequest]); 131 | [self waitForExpectations:@[authRequestExpectation] timeout:self.expectationTimeout]; 132 | XCTAssertFalse([authSession isAuthorizedRequest:secureRequest]); 133 | } 134 | 135 | - (void)testAuthorizeInsecureRequestWithCompletionError { 136 | XCTestExpectation *authRequestExpectation = 137 | [[XCTestExpectation alloc] initWithDescription:@"Authorize with completion"]; 138 | 139 | GTMAuthSession *authSession = 140 | [[GTMAuthSession alloc] initWithAuthState:[OIDAuthState testInstance]]; 141 | NSMutableURLRequest *secureRequest = [NSMutableURLRequest requestWithURL:self.insecureURL]; 142 | 143 | [authSession authorizeRequest:secureRequest completionHandler:^(NSError * _Nullable error) { 144 | XCTAssertNotNil(error); 145 | XCTAssertEqual(error.code, GTMAuthSessionErrorCodeCannotAuthorizeRequest); 146 | [authRequestExpectation fulfill]; 147 | }]; 148 | 149 | XCTAssertTrue([authSession isAuthorizingRequest:secureRequest]); 150 | [self waitForExpectations:@[authRequestExpectation] timeout:self.expectationTimeout]; 151 | XCTAssertFalse([authSession isAuthorizedRequest:secureRequest]); 152 | } 153 | 154 | - (void)testAuthorizeSecureRequestWithDelegate { 155 | XCTestExpectation *delegateExpectation = 156 | [[XCTestExpectation alloc] initWithDescription:@"Authorize with delegate"]; 157 | 158 | OIDAuthState *authState = OIDAuthState.testInstance; 159 | GTMAuthSession *originalAuthorization = 160 | [[GTMAuthSession alloc] initWithAuthState:authState]; 161 | GTMAuthorizationTestDelegate *testingDelegate = 162 | [[GTMAuthorizationTestDelegate alloc] initWithExpectation:delegateExpectation]; 163 | 164 | NSMutableURLRequest *originalRequest = [[NSMutableURLRequest alloc] initWithURL:self.secureURL]; 165 | [originalAuthorization authorizeRequest:originalRequest 166 | delegate:testingDelegate 167 | didFinishSelector:@selector(authentication:request:finishedWithError:)]; 168 | 169 | [self waitForExpectations:@[delegateExpectation] timeout:self.expectationTimeout]; 170 | 171 | XCTAssertNotNil(testingDelegate.passedRequest); 172 | XCTAssertEqualObjects(originalRequest, testingDelegate.passedRequest); 173 | XCTAssertNotNil(testingDelegate.passedAuthorization); 174 | XCTAssertEqual(originalAuthorization, testingDelegate.passedAuthorization); 175 | XCTAssertNil(testingDelegate.passedError); 176 | } 177 | 178 | - (void)testStopAuthorization { 179 | XCTestExpectation *authorizeSecureRequestExpectation = 180 | [self expectationWithDescription:@"Authorize with completion expectation"]; 181 | 182 | GTMAuthSession *authSession = 183 | [[GTMAuthSession alloc] initWithAuthState:OIDAuthState.testInstance]; 184 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.secureURL]; 185 | 186 | [authSession authorizeRequest:request completionHandler:^(NSError * _Nullable error) { 187 | XCTAssertNil(error); 188 | [authorizeSecureRequestExpectation fulfill]; 189 | }]; 190 | 191 | XCTAssertTrue([authSession isAuthorizingRequest:request]); 192 | [authSession stopAuthorization]; 193 | XCTAssertFalse([authSession isAuthorizingRequest:request]); 194 | [authorizeSecureRequestExpectation fulfill]; 195 | [self waitForExpectations:@[authorizeSecureRequestExpectation] timeout:self.expectationTimeout]; 196 | } 197 | 198 | - (void)testStopAuthorizationForRequest { 199 | XCTestExpectation *authorizeSecureRequestExpectation = 200 | [self expectationWithDescription:@"Authorize with completion expectation"]; 201 | 202 | GTMAuthSession *authSession = 203 | [[GTMAuthSession alloc] initWithAuthState:OIDAuthState.testInstance]; 204 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.secureURL]; 205 | 206 | [authSession authorizeRequest:request completionHandler:^(NSError * _Nullable error) { 207 | XCTAssertNil(error); 208 | [authorizeSecureRequestExpectation fulfill]; 209 | }]; 210 | 211 | XCTAssertTrue([authSession isAuthorizingRequest:request]); 212 | [authSession stopAuthorizationForRequest:request]; 213 | XCTAssertFalse([authSession isAuthorizingRequest:request]); 214 | [authorizeSecureRequestExpectation fulfill]; 215 | [self waitForExpectations:@[authorizeSecureRequestExpectation] timeout:self.expectationTimeout]; 216 | } 217 | 218 | - (void)testIsAuthorizedRequest { 219 | GTMAuthSession *authSession = 220 | [[GTMAuthSession alloc] initWithAuthState:OIDAuthState.testInstance]; 221 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.secureURL]; 222 | XCTAssertFalse([authSession isAuthorizedRequest:request]); 223 | } 224 | 225 | - (void)testCanAuthorizeRequest { 226 | GTMAuthSession *authSession = 227 | [[GTMAuthSession alloc] initWithAuthState:OIDAuthState.testInstance]; 228 | XCTAssertTrue([authSession canAuthorize]); 229 | } 230 | 231 | - (void)testCannotAuthorizeRequest { 232 | OIDAuthState *testAuthState = 233 | [OIDAuthState testInstanceWithAuthorizationResponse:nil 234 | tokenResponse:nil 235 | registrationResponse:OIDRegistrationResponse.testInstance]; 236 | GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:testAuthState]; 237 | XCTAssertFalse([authSession canAuthorize]); 238 | } 239 | 240 | - (void)testIsNotPrimeForRefresh { 241 | GTMAuthSession *authSession = 242 | [[GTMAuthSession alloc] initWithAuthState:OIDAuthState.testInstance]; 243 | XCTAssertFalse([authSession primeForRefresh]); 244 | } 245 | 246 | - (void)testIsPrimeForRefresh { 247 | OIDAuthState *testAuthState = 248 | [OIDAuthState testInstanceWithAuthorizationResponse:nil 249 | tokenResponse:nil 250 | registrationResponse:OIDRegistrationResponse.testInstance]; 251 | GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:testAuthState]; 252 | XCTAssertTrue([authSession primeForRefresh]); 253 | } 254 | 255 | - (void)testConfigurationForGoogle { 256 | OIDServiceConfiguration *configuration = [GTMAuthSession configurationForGoogle]; 257 | XCTAssertNotNil(configuration); 258 | XCTAssertEqualObjects(configuration.authorizationEndpoint, self.googleAuthzEndpoint); 259 | XCTAssertEqualObjects(configuration.tokenEndpoint, self.tokenEndpoint); 260 | } 261 | 262 | @end 263 | 264 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/ObjCIntegration/KeychainStore/GTMKeychainStoreTests.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | #if SWIFT_PACKAGE 20 | @import AppAuthCore; 21 | @import TestHelpers; 22 | #else 23 | @import AppAuth; 24 | #import "GTMAppAuth_Unit_objc_api_integration-Swift.h" 25 | #endif 26 | @import GTMAppAuth; 27 | 28 | @interface GTMKeychainStoreTests : XCTestCase 29 | 30 | @property (nonatomic) GTMAuthSession *authSession; 31 | @property (nonatomic) GTMKeychainStore *keychainStore; 32 | 33 | @end 34 | 35 | @implementation GTMKeychainStoreTests 36 | 37 | - (void)setUp { 38 | self.authSession = [[GTMAuthSession alloc] initWithAuthState:[OIDAuthState testInstance]]; 39 | 40 | NSSet *emptyKeychainAttributes = [NSSet set]; 41 | GTMKeychainHelperFake *fakeKeychain = 42 | [[GTMKeychainHelperFake alloc] initWithKeychainAttributes:emptyKeychainAttributes]; 43 | self.keychainStore = 44 | [[GTMKeychainStore alloc] initWithItemName:[GTMTestingConstants testKeychainItemName] 45 | keychainAttributes:emptyKeychainAttributes 46 | keychainHelper:fakeKeychain]; 47 | [super setUp]; 48 | } 49 | 50 | - (void)testInitWithItemName { 51 | GTMKeychainStore *keychainStore = 52 | [[GTMKeychainStore alloc] initWithItemName:[GTMTestingConstants testKeychainItemName]]; 53 | XCTAssertNotNil(keychainStore); 54 | } 55 | 56 | - (void)testInitWithItemNameKeychainAttributes { 57 | GTMKeychainStore *keychainStore = 58 | [[GTMKeychainStore alloc] initWithItemName:[GTMTestingConstants testKeychainItemName] 59 | keychainAttributes:[NSSet set]]; 60 | XCTAssertNotNil(keychainStore); 61 | } 62 | 63 | - (void)testInitWithItemNameKeychainHelper { 64 | GTMKeychainHelperFake *fakeKeychain = 65 | [[GTMKeychainHelperFake alloc] initWithKeychainAttributes:[NSSet set]]; 66 | GTMKeychainStore *keychainStore = 67 | [[GTMKeychainStore alloc] initWithItemName:[GTMTestingConstants testKeychainItemName] 68 | keychainHelper:fakeKeychain]; 69 | XCTAssertNotNil(keychainStore); 70 | } 71 | 72 | - (void)testInitWithItemNameKeychainAttributesKeychainHelper { 73 | GTMKeychainHelperFake *fakeKeychain = 74 | [[GTMKeychainHelperFake alloc] initWithKeychainAttributes:[NSSet set]]; 75 | GTMKeychainStore *keychainStore = 76 | [[GTMKeychainStore alloc] initWithItemName:[GTMTestingConstants testKeychainItemName] 77 | keychainAttributes:[NSSet set] 78 | keychainHelper:fakeKeychain]; 79 | XCTAssertNotNil(keychainStore); 80 | } 81 | 82 | - (void)testSaveAuthorization { 83 | NSError *error; 84 | [self.keychainStore saveAuthSession:self.authSession error:&error]; 85 | XCTAssertNil(error); 86 | } 87 | 88 | - (void)testSavingWithNewItemName { 89 | NSString *newItemName = @"newItemName"; 90 | self.keychainStore.itemName = newItemName; 91 | 92 | NSError *error; 93 | [self.keychainStore saveAuthSession:self.authSession 94 | withItemName:newItemName 95 | error:&error]; 96 | XCTAssertNil(error); 97 | 98 | XCTAssertEqualObjects(newItemName, self.keychainStore.itemName); 99 | GTMAuthSession *authSession = 100 | [self.keychainStore retrieveAuthSessionWithItemName:self.keychainStore.itemName error:&error]; 101 | XCTAssertNotNil(authSession); 102 | 103 | XCTAssertEqual(authSession.authState.isAuthorized, self.authSession.authState.isAuthorized); 104 | XCTAssertEqualObjects(authSession.serviceProvider, self.authSession.serviceProvider); 105 | XCTAssertEqualObjects(authSession.userID, self.authSession.userID); 106 | XCTAssertEqualObjects(authSession.userEmail, self.authSession.userEmail); 107 | XCTAssertEqual(authSession.userEmailIsVerified, self.authSession.userEmailIsVerified); 108 | } 109 | 110 | - (void)testSavingWithCustomItemName { 111 | NSString *customItemName = @"customItemName"; 112 | NSError *error; 113 | 114 | [self.keychainStore saveAuthSession:self.authSession 115 | withItemName:customItemName 116 | error:&error]; 117 | XCTAssertNil(error); 118 | 119 | GTMAuthSession *retrievedAuth = 120 | [self.keychainStore retrieveAuthSessionWithItemName:customItemName error:&error]; 121 | XCTAssertNotNil(retrievedAuth); 122 | XCTAssertNil(error); 123 | 124 | XCTAssertNotNil(retrievedAuth); 125 | 126 | XCTAssertEqual(retrievedAuth.authState.isAuthorized, 127 | self.authSession.authState.isAuthorized); 128 | XCTAssertEqualObjects(retrievedAuth.serviceProvider, self.authSession.serviceProvider); 129 | XCTAssertEqualObjects(retrievedAuth.userID, self.authSession.userID); 130 | XCTAssertEqualObjects(retrievedAuth.userEmail, self.authSession.userEmail); 131 | XCTAssertEqual(retrievedAuth.userEmailIsVerified, self.authSession.userEmailIsVerified); 132 | } 133 | 134 | - (void)testSaveAuthSessionErrorNoServiceName { 135 | NSError *error; 136 | [self.keychainStore saveAuthSession:self.authSession 137 | withItemName:@"" 138 | error:&error]; 139 | XCTAssertNotNil(error); 140 | XCTAssertEqualObjects(error.domain, @"GTMAppAuthKeychainErrorDomain"); 141 | XCTAssertEqual(error.code, GTMKeychainStoreErrorCodeNoService); 142 | } 143 | 144 | - (void)testRetrieveAuthSession { 145 | NSError *error; 146 | [self.keychainStore saveAuthSession:self.authSession error:&error]; 147 | XCTAssertNil(error); 148 | 149 | GTMAuthSession *authSession = [self.keychainStore retrieveAuthSessionWithError:&error]; 150 | XCTAssertNil(error); 151 | 152 | XCTAssertNotNil(authSession); 153 | XCTAssertEqual(authSession.authState.isAuthorized, self.authSession.authState.isAuthorized); 154 | XCTAssertEqualObjects(authSession.serviceProvider, self.authSession.serviceProvider); 155 | XCTAssertEqualObjects(authSession.userID, self.authSession.userID); 156 | XCTAssertEqualObjects(authSession.userEmail, self.authSession.userEmail); 157 | XCTAssertEqual(authSession.userEmailIsVerified, self.authSession.userEmailIsVerified); 158 | } 159 | 160 | - (void)testRetrieveAuthSessionForMissingItemName { 161 | NSError *error; 162 | NSString *missingItemName = @"missingItemName"; 163 | GTMAuthSession *missingAuthSession = 164 | [self.keychainStore retrieveAuthSessionWithItemName:missingItemName error:&error]; 165 | 166 | XCTAssertNil(missingAuthSession); 167 | XCTAssertNotNil(error); 168 | XCTAssertEqual(error.code, GTMKeychainStoreErrorCodePasswordNotFound); 169 | } 170 | 171 | - (void)testRetrieveAuthSessionForCustomItemName { 172 | NSError *error; 173 | NSString *customItemName = @"customItemName"; 174 | self.keychainStore.itemName = customItemName; 175 | [self.keychainStore saveAuthSession:self.authSession withItemName:customItemName error:&error]; 176 | XCTAssertNil(error); 177 | 178 | GTMAuthSession *retrievedAuthSession = 179 | [self.keychainStore retrieveAuthSessionWithError:&error]; 180 | XCTAssertNotNil(retrievedAuthSession); 181 | XCTAssertNotNil(retrievedAuthSession); 182 | XCTAssertEqual(retrievedAuthSession.authState.isAuthorized, 183 | self.authSession.authState.isAuthorized); 184 | XCTAssertEqualObjects(retrievedAuthSession.serviceProvider, self.authSession.serviceProvider); 185 | XCTAssertEqualObjects(retrievedAuthSession.userID, self.authSession.userID); 186 | XCTAssertEqualObjects(retrievedAuthSession.userEmail, self.authSession.userEmail); 187 | XCTAssertEqual(retrievedAuthSession.userEmailIsVerified, self.authSession.userEmailIsVerified); 188 | } 189 | 190 | - (void)testRemoveAuthSession { 191 | NSError *error; 192 | [self.keychainStore saveAuthSession:self.authSession error:&error]; 193 | XCTAssertNil(error); 194 | 195 | [self.keychainStore removeAuthSessionWithError:&error]; 196 | XCTAssertNil(error); 197 | } 198 | 199 | - (void)testRemoveAuthSessionForMissingItemNameThrowsError { 200 | NSError *error; 201 | NSString *missingItemName = @"missingItemName"; 202 | [self.keychainStore saveAuthSession:self.authSession error:&error]; 203 | XCTAssertNil(error); 204 | 205 | [self.keychainStore removeAuthSessionWithItemName:missingItemName error:&error]; 206 | XCTAssertNotNil(error); 207 | XCTAssertEqual(error.code, GTMKeychainStoreErrorCodeFailedToDeletePasswordBecauseItemNotFound); 208 | } 209 | 210 | - (void)testKeychainStoreAttributes { 211 | NSString *testAccessGroupName = @"testKeychainAccessGroup"; 212 | GTMKeychainAttribute *keychainAccessGroup = 213 | [GTMKeychainAttribute keychainAccessGroupWithName:testAccessGroupName]; 214 | NSSet *attributes = [NSSet setWithArray:@[keychainAccessGroup]]; 215 | 216 | GTMKeychainStore *keychainStore = 217 | [[GTMKeychainStore alloc] initWithItemName:[GTMTestingConstants testKeychainItemName] 218 | keychainAttributes:attributes]; 219 | XCTAssertTrue(keychainStore.keychainAttributes.count > 0); 220 | XCTAssertFalse([keychainStore.keychainAttributes 221 | containsObject:[GTMKeychainAttribute useFileBasedKeychain]]); 222 | XCTAssertTrue([keychainStore.keychainAttributes containsObject:keychainAccessGroup]); 223 | } 224 | 225 | - (void)testKeychainStoreWithFileBasedAttribute { 226 | GTMKeychainAttribute *useFileBasedKeychain = [GTMKeychainAttribute useFileBasedKeychain]; 227 | NSSet *attributes = 228 | [NSSet setWithArray:@[ 229 | useFileBasedKeychain 230 | ]]; 231 | 232 | GTMKeychainStore *keychainStore = 233 | [[GTMKeychainStore alloc] initWithItemName:[GTMTestingConstants testKeychainItemName] 234 | keychainAttributes:attributes]; 235 | XCTAssertTrue(keychainStore.keychainAttributes.count > 0); 236 | XCTAssertTrue([keychainStore.keychainAttributes containsObject:useFileBasedKeychain]); 237 | } 238 | 239 | - (void)testSaveAuthSessionInGTMOAuth2Format { 240 | NSError *error; 241 | [self.keychainStore saveWithGTMOAuth2FormatForAuthSession:self.authSession error:&error]; 242 | XCTAssertNil(error); 243 | } 244 | 245 | - (void)testRetrieveAuthSessionInGTMOAuth2Format { 246 | NSError *error; 247 | GTMAuthSession *expectedAuthSession = 248 | [[GTMAuthSession alloc] initWithAuthState:[OIDAuthState testInstance] 249 | serviceProvider:[GTMTestingConstants testServiceProvider] 250 | userID:[GTMTestingConstants testUserID] 251 | userEmail:[GTMTestingConstants testEmail] 252 | userEmailIsVerified:@"y"]; 253 | [self.keychainStore saveWithGTMOAuth2FormatForAuthSession:expectedAuthSession error:&error]; 254 | XCTAssertNil(error); 255 | 256 | GTMAuthSession *testAuth = 257 | [self.keychainStore 258 | retrieveAuthSessionInGTMOAuth2FormatWithTokenURL:[GTMTestingConstants testTokenURL] 259 | redirectURI:[GTMTestingConstants testRedirectURI] 260 | clientID:[GTMTestingConstants testClientID] 261 | clientSecret:[GTMTestingConstants testClientSecret] 262 | error:&error]; 263 | 264 | XCTAssertNil(error); 265 | XCTAssertEqualObjects(testAuth.authState.scope, expectedAuthSession.authState.scope); 266 | XCTAssertEqualObjects(testAuth.authState.lastTokenResponse.accessToken, 267 | expectedAuthSession.authState.lastTokenResponse.accessToken); 268 | XCTAssertEqualObjects(testAuth.authState.refreshToken, 269 | expectedAuthSession.authState.refreshToken); 270 | XCTAssertEqual(testAuth.authState.isAuthorized, expectedAuthSession.authState.isAuthorized); 271 | XCTAssertEqualObjects(testAuth.serviceProvider, expectedAuthSession.serviceProvider); 272 | XCTAssertEqualObjects(testAuth.userID, expectedAuthSession.userID); 273 | XCTAssertEqualObjects(testAuth.userEmail, expectedAuthSession.userEmail); 274 | XCTAssertEqual(testAuth.userEmailIsVerified, expectedAuthSession.userEmailIsVerified); 275 | XCTAssertEqual(testAuth.canAuthorize, expectedAuthSession.canAuthorize); 276 | } 277 | 278 | - (void)testRemoveAuthSessionInGTMOAuth2Format { 279 | NSError *error; 280 | [self.keychainStore saveWithGTMOAuth2FormatForAuthSession:self.authSession error:&error]; 281 | XCTAssertNil(error); 282 | 283 | [self.keychainStore removeAuthSessionWithError:&error]; 284 | XCTAssertNil(error); 285 | } 286 | 287 | @end 288 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/ObjCIntegration/KeychainStore/GTMOAuth2CompatibilityTests.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | #if SWIFT_PACKAGE 20 | @import AppAuthCore; 21 | @import TestHelpers; 22 | #else 23 | @import AppAuth; 24 | #import "GTMAppAuth_Unit_objc_api_integration-Swift.h" 25 | #endif 26 | @import GTMAppAuth; 27 | 28 | @interface GTMOAuth2CompatibilityTests : XCTestCase 29 | 30 | @property (nonatomic) GTMAuthSession *expectedAuthSession; 31 | 32 | @end 33 | 34 | @implementation GTMOAuth2CompatibilityTests 35 | 36 | - (void)setUp { 37 | self.expectedAuthSession = 38 | [[GTMAuthSession alloc] initWithAuthState:OIDAuthState.testInstance 39 | serviceProvider:GTMTestingConstants.testServiceProvider 40 | userID:GTMTestingConstants.userID 41 | userEmail:GTMTestingConstants.testEmail 42 | userEmailIsVerified:@"y"]; 43 | [super setUp]; 44 | } 45 | 46 | - (NSString *)expectedPersistenceResponseString { 47 | return [NSString stringWithFormat:@"access_token=%@&refresh_token=%@&scope=%@&serviceProvider=%@&userEmail=foo%%40foo.com&userEmailIsVerified=y&userID=%@", GTMTestingConstants.testAccessToken, GTMTestingConstants.testRefreshToken, GTMTestingConstants.testScope2, GTMTestingConstants.testServiceProvider, GTMTestingConstants.testUserID]; 48 | } 49 | 50 | - (void)testPersistenceResponseString { 51 | NSString *testPersistenceResponseString = 52 | [GTMOAuth2Compatibility persistenceResponseStringForAuthSession:self.expectedAuthSession]; 53 | XCTAssertNotNil(testPersistenceResponseString); 54 | XCTAssertEqualObjects([self expectedPersistenceResponseString], testPersistenceResponseString); 55 | } 56 | 57 | - (void)testAuthStateForPersistenceString { 58 | NSError *error; 59 | GTMAuthSession *testPersistAuthSession = 60 | [GTMOAuth2Compatibility authSessionForPersistenceString:[self expectedPersistenceResponseString] 61 | tokenURL:GTMTestingConstants.testTokenURL 62 | redirectURI:GTMTestingConstants.testRedirectURI 63 | clientID:GTMTestingConstants.testClientID 64 | clientSecret:GTMTestingConstants.testClientSecret 65 | error:&error]; 66 | XCTAssertNil(error); 67 | XCTAssertEqual(testPersistAuthSession.authState.scope, self.expectedAuthSession.authState.scope); 68 | XCTAssertEqualObjects(testPersistAuthSession.authState.lastTokenResponse.accessToken, 69 | self.expectedAuthSession.authState.lastTokenResponse.accessToken); 70 | XCTAssertEqualObjects(testPersistAuthSession.authState.refreshToken, 71 | self.expectedAuthSession.authState.refreshToken); 72 | XCTAssertEqual(testPersistAuthSession.authState.isAuthorized, 73 | self.expectedAuthSession.authState.isAuthorized); 74 | XCTAssertEqualObjects(testPersistAuthSession.serviceProvider, 75 | self.expectedAuthSession.serviceProvider); 76 | XCTAssertEqualObjects(testPersistAuthSession.userID, self.expectedAuthSession.userID); 77 | XCTAssertEqualObjects(testPersistAuthSession.userEmail, self.expectedAuthSession.userEmail); 78 | XCTAssertEqual(testPersistAuthSession.userEmailIsVerified, 79 | self.expectedAuthSession.userEmailIsVerified); 80 | XCTAssertEqual(testPersistAuthSession.canAuthorize, self.expectedAuthSession.canAuthorize); 81 | } 82 | 83 | - (void)testAuthSessionForPersistenceStringThrows { 84 | NSError *error; 85 | GTMAuthSession *testPersistAuthSession __unused = 86 | [GTMOAuth2Compatibility authSessionForPersistenceString:[self expectedPersistenceResponseString] 87 | tokenURL:GTMTestingConstants.testTokenURL 88 | redirectURI:@"" 89 | clientID:GTMTestingConstants.testClientID 90 | clientSecret:GTMTestingConstants.testClientSecret 91 | error:&error]; 92 | XCTAssertNotNil(error); 93 | XCTAssertEqual(error.code, GTMKeychainStoreErrorCodeFailedToConvertRedirectURItoURL); 94 | } 95 | 96 | @end 97 | -------------------------------------------------------------------------------- /GTMAppAuth/Tests/Unit/KeychainStore/GTMOAuth2CompatibilityTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | // Ensure that we import the correct dependency for both SPM and CocoaPods since 19 | // the latter doesn't define separate Clang modules for subspecs 20 | #if SWIFT_PACKAGE 21 | import AppAuthCore 22 | import TestHelpers 23 | #else 24 | import AppAuth 25 | #endif 26 | @testable import GTMAppAuth 27 | 28 | class GTMOAuth2CompatibilityTests: XCTestCase { 29 | private lazy var testPersistenceString: String = { 30 | return "access_token=\(TestingConstants.testAccessToken)&refresh_token=\(TestingConstants.testRefreshToken)&scope=\(TestingConstants.testScope2)&serviceProvider=\(TestingConstants.testServiceProvider)&userEmail=foo%40foo.com&userEmailIsVerified=y&userID=\(TestingConstants.testUserID)" 31 | }() 32 | private lazy var keychainStoreWithAttributes: KeychainStore = { 33 | let attributes: Set = [ 34 | .keychainAccessGroup(name: TestingConstants.testAccessGroup) 35 | ] 36 | let keychainHelperWithAttributes = KeychainHelperFake(keychainAttributes: attributes) 37 | return KeychainStore( 38 | itemName: TestingConstants.testKeychainItemName, 39 | keychainHelper: keychainHelperWithAttributes 40 | ) 41 | }() 42 | private let keychainHelper = KeychainHelperFake(keychainAttributes: []) 43 | private lazy var keychainStore: KeychainStore = { 44 | return KeychainStore( 45 | itemName: TestingConstants.testKeychainItemName, 46 | keychainHelper: keychainHelper 47 | ) 48 | }() 49 | private var expectedAuthSession: AuthSession { 50 | AuthSession( 51 | authState: OIDAuthState.testInstance(), 52 | serviceProvider: TestingConstants.testServiceProvider, 53 | userID: TestingConstants.testUserID, 54 | userEmail: TestingConstants.testEmail, 55 | userEmailIsVerified: "y" 56 | ) 57 | } 58 | 59 | override func tearDown() { 60 | super.tearDown() 61 | keychainHelper.passwordStore.removeAll() 62 | } 63 | 64 | func testPersistenceResponseString() { 65 | let response = GTMOAuth2Compatibility.persistenceResponseString( 66 | forAuthSession: expectedAuthSession 67 | ) 68 | guard let response = response else { 69 | return XCTFail("Response shouldn't be nil") 70 | } 71 | XCTAssertEqual(response, testPersistenceString) 72 | } 73 | 74 | func testSaveOAuth2AuthSession() throws { 75 | try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 76 | // Save with keychain attributes to simulate macOS environment with 77 | // `kSecUseDataProtectionKeychain` 78 | try keychainStoreWithAttributes.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 79 | } 80 | 81 | func testSaveGTMOAuth2AuthSessionThrowsError() { 82 | let emptyItemName = "" 83 | keychainStore.itemName = emptyItemName 84 | XCTAssertThrowsError( 85 | try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 86 | ) { thrownError in 87 | XCTAssertEqual( 88 | thrownError as? KeychainStore.Error, 89 | KeychainStore.Error.noService 90 | ) 91 | } 92 | } 93 | 94 | func testRemoveOAuth2AuthSession() throws { 95 | try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 96 | let _ = try keychainStore.retrieveAuthSessionInGTMOAuth2Format( 97 | tokenURL: TestingConstants.testTokenURL, 98 | redirectURI: TestingConstants.testRedirectURI, 99 | clientID: TestingConstants.testClientID, 100 | clientSecret: TestingConstants.testClientSecret 101 | ) 102 | try keychainStore.removeAuthSession() 103 | XCTAssertThrowsError(try keychainStore.retrieveAuthSession()) { thrownError in 104 | XCTAssertEqual( 105 | thrownError as? KeychainStore.Error, 106 | KeychainStore.Error.passwordNotFound(forItemName: TestingConstants.testKeychainItemName) 107 | ) 108 | } 109 | 110 | try keychainStoreWithAttributes.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 111 | let _ = try keychainStoreWithAttributes.retrieveAuthSessionInGTMOAuth2Format( 112 | tokenURL: TestingConstants.testTokenURL, 113 | redirectURI: TestingConstants.testRedirectURI, 114 | clientID: TestingConstants.testClientID, 115 | clientSecret: TestingConstants.testClientSecret 116 | ) 117 | try keychainStoreWithAttributes.removeAuthSession() 118 | XCTAssertThrowsError(try keychainStoreWithAttributes.retrieveAuthSession()) { thrownError in 119 | XCTAssertEqual( 120 | thrownError as? KeychainStore.Error, 121 | KeychainStore.Error.passwordNotFound(forItemName: TestingConstants.testKeychainItemName) 122 | ) 123 | } 124 | } 125 | 126 | func testRemoveOAuth2AuthSessionhrowsError() { 127 | XCTAssertThrowsError( 128 | try keychainStore.removeAuthSession() 129 | ) { thrownError in 130 | XCTAssertEqual( 131 | thrownError as? KeychainStore.Error, 132 | .failedToDeletePasswordBecauseItemNotFound(itemName: TestingConstants.testKeychainItemName) 133 | ) 134 | } 135 | } 136 | 137 | func testAuthSessionFromKeychainForName() throws { 138 | try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 139 | let authSession = try keychainStore.retrieveAuthSessionInGTMOAuth2Format( 140 | tokenURL: TestingConstants.testTokenURL, 141 | redirectURI: TestingConstants.testRedirectURI, 142 | clientID: TestingConstants.testClientID, 143 | clientSecret: TestingConstants.testClientID 144 | ) 145 | 146 | XCTAssertEqual(authSession.authState.scope, expectedAuthSession.authState.scope) 147 | XCTAssertEqual( 148 | authSession.authState.lastTokenResponse?.accessToken, 149 | expectedAuthSession.authState.lastTokenResponse?.accessToken 150 | ) 151 | XCTAssertEqual(authSession.authState.refreshToken, expectedAuthSession.authState.refreshToken) 152 | XCTAssertEqual(authSession.authState.isAuthorized, expectedAuthSession.authState.isAuthorized) 153 | XCTAssertEqual(authSession.serviceProvider, expectedAuthSession.serviceProvider) 154 | XCTAssertEqual(authSession.userID, expectedAuthSession.userID) 155 | XCTAssertEqual(authSession.userEmail, expectedAuthSession.userEmail) 156 | XCTAssertEqual(authSession.userEmailIsVerified, expectedAuthSession.userEmailIsVerified) 157 | XCTAssertEqual(authSession.canAuthorize, expectedAuthSession.canAuthorize) 158 | } 159 | 160 | func testAuthSessionFromKeychainWithAttributesForName() throws { 161 | try keychainStoreWithAttributes.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 162 | let authSession = try keychainStoreWithAttributes.retrieveAuthSessionInGTMOAuth2Format( 163 | tokenURL: TestingConstants.testTokenURL, 164 | redirectURI: TestingConstants.testRedirectURI, 165 | clientID: TestingConstants.testClientID, 166 | clientSecret: TestingConstants.testClientID 167 | ) 168 | 169 | XCTAssertEqual(authSession.authState.scope, expectedAuthSession.authState.scope) 170 | XCTAssertEqual( 171 | authSession.authState.lastTokenResponse?.accessToken, 172 | expectedAuthSession.authState.lastTokenResponse?.accessToken 173 | ) 174 | XCTAssertEqual(authSession.authState.refreshToken, expectedAuthSession.authState.refreshToken) 175 | XCTAssertEqual(authSession.authState.isAuthorized, expectedAuthSession.authState.isAuthorized) 176 | XCTAssertEqual(authSession.serviceProvider, expectedAuthSession.serviceProvider) 177 | XCTAssertEqual(authSession.userID, expectedAuthSession.userID) 178 | XCTAssertEqual(authSession.userEmail, expectedAuthSession.userEmail) 179 | XCTAssertEqual(authSession.userEmailIsVerified, expectedAuthSession.userEmailIsVerified) 180 | XCTAssertEqual(authSession.canAuthorize, expectedAuthSession.canAuthorize) 181 | } 182 | 183 | func testAuthSessionFromKeychainForNameThrowsError() throws { 184 | try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 185 | let badRedirectURI = "" 186 | XCTAssertThrowsError( 187 | _ = try keychainStore.retrieveAuthSessionInGTMOAuth2Format( 188 | tokenURL: TestingConstants.testTokenURL, 189 | redirectURI: badRedirectURI, 190 | clientID: TestingConstants.testClientID, 191 | clientSecret: TestingConstants.testClientSecret 192 | ) 193 | ) { thrownError in 194 | XCTAssertEqual( 195 | thrownError as? KeychainStore.Error, 196 | .failedToConvertRedirectURItoURL(badRedirectURI) 197 | ) 198 | } 199 | } 200 | 201 | func testAuthSessionFromKeychainForPersistenceStringFailedWithBadURI() { 202 | let badURI = "" 203 | XCTAssertThrowsError( 204 | try GTMOAuth2Compatibility.authSession( 205 | forPersistenceString: testPersistenceString, 206 | tokenURL: TestingConstants.testTokenURL, 207 | redirectURI: badURI, 208 | clientID: TestingConstants.testClientID, 209 | clientSecret: TestingConstants.testClientSecret 210 | ) 211 | ) { thrownError in 212 | XCTAssertEqual( 213 | thrownError as? KeychainStore.Error, 214 | .failedToConvertRedirectURItoURL(badURI) 215 | ) 216 | } 217 | } 218 | 219 | func testAuthSessionFromKeychainForPersistenceString() throws { 220 | let authSession = try GTMOAuth2Compatibility.authSession( 221 | forPersistenceString: testPersistenceString, 222 | tokenURL: TestingConstants.testTokenURL, 223 | redirectURI: TestingConstants.testRedirectURI, 224 | clientID: TestingConstants.testClientID, 225 | clientSecret: TestingConstants.testClientSecret 226 | ) 227 | 228 | XCTAssertEqual(authSession.authState.scope, expectedAuthSession.authState.scope) 229 | XCTAssertEqual( 230 | authSession.authState.lastTokenResponse?.accessToken, 231 | expectedAuthSession.authState.lastTokenResponse?.accessToken 232 | ) 233 | XCTAssertEqual(authSession.authState.refreshToken, expectedAuthSession.authState.refreshToken) 234 | XCTAssertEqual(authSession.authState.isAuthorized, expectedAuthSession.authState.isAuthorized) 235 | XCTAssertEqual(authSession.serviceProvider, expectedAuthSession.serviceProvider) 236 | XCTAssertEqual(authSession.userID, expectedAuthSession.userID) 237 | XCTAssertEqual(authSession.userEmail, expectedAuthSession.userEmail) 238 | XCTAssertEqual(authSession.userEmailIsVerified, expectedAuthSession.userEmailIsVerified) 239 | XCTAssertEqual(authSession.canAuthorize, expectedAuthSession.canAuthorize) 240 | } 241 | 242 | func testAuthSessionFromKeychainMatchesForNameAndPersistenceString() throws { 243 | let expectedPersistAuth = try GTMOAuth2Compatibility.authSession( 244 | forPersistenceString: testPersistenceString, 245 | tokenURL: TestingConstants.testTokenURL, 246 | redirectURI: TestingConstants.testRedirectURI, 247 | clientID: TestingConstants.testClientID, 248 | clientSecret: TestingConstants.testClientSecret 249 | ) 250 | try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedPersistAuth) 251 | 252 | let testPersistAuth = try keychainStore.retrieveAuthSessionInGTMOAuth2Format( 253 | tokenURL: TestingConstants.testTokenURL, 254 | redirectURI: TestingConstants.testRedirectURI, 255 | clientID: TestingConstants.testClientID, 256 | clientSecret: TestingConstants.testClientSecret 257 | ) 258 | 259 | XCTAssertEqual(testPersistAuth.authState.scope, expectedPersistAuth.authState.scope) 260 | XCTAssertEqual( 261 | testPersistAuth.authState.lastTokenResponse?.accessToken, 262 | expectedPersistAuth.authState.lastTokenResponse?.accessToken 263 | ) 264 | XCTAssertEqual( 265 | testPersistAuth.authState.refreshToken, 266 | expectedPersistAuth.authState.refreshToken 267 | ) 268 | XCTAssertEqual( 269 | testPersistAuth.authState.isAuthorized, 270 | expectedPersistAuth.authState.isAuthorized 271 | ) 272 | XCTAssertEqual(testPersistAuth.serviceProvider, expectedPersistAuth.serviceProvider) 273 | XCTAssertEqual(testPersistAuth.userID, expectedPersistAuth.userID) 274 | XCTAssertEqual(testPersistAuth.userEmail, expectedPersistAuth.userEmail) 275 | XCTAssertEqual(testPersistAuth.userEmailIsVerified, expectedPersistAuth.userEmailIsVerified) 276 | XCTAssertEqual(testPersistAuth.canAuthorize, expectedPersistAuth.canAuthorize) 277 | } 278 | 279 | func testAuthSessionFromKeychainUsingGoogleOAuthProviderInformation() throws { 280 | let expectedPersistAuth = try GTMOAuth2Compatibility.authSession( 281 | forPersistenceString: testPersistenceString, 282 | tokenURL: TestingConstants.testTokenURL, 283 | redirectURI: TestingConstants.testRedirectURI, 284 | clientID: TestingConstants.testClientID, 285 | clientSecret: TestingConstants.testClientSecret 286 | ) 287 | try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) 288 | 289 | let testAuthSession = try keychainStore.retrieveAuthSessionForGoogleInGTMOAuth2Format( 290 | clientID: TestingConstants.testClientID, 291 | clientSecret: TestingConstants.testClientSecret 292 | ) 293 | 294 | XCTAssertEqual(testAuthSession.authState.scope, expectedPersistAuth.authState.scope) 295 | XCTAssertEqual( 296 | testAuthSession.authState.lastTokenResponse?.accessToken, 297 | expectedPersistAuth.authState.lastTokenResponse?.accessToken 298 | ) 299 | XCTAssertEqual(testAuthSession.authState.refreshToken, expectedPersistAuth.authState.refreshToken) 300 | XCTAssertEqual(testAuthSession.authState.isAuthorized, expectedPersistAuth.authState.isAuthorized) 301 | XCTAssertEqual(testAuthSession.serviceProvider, expectedPersistAuth.serviceProvider) 302 | XCTAssertEqual(testAuthSession.userID, expectedPersistAuth.userID) 303 | XCTAssertEqual(testAuthSession.userEmail, expectedPersistAuth.userEmail) 304 | XCTAssertEqual(testAuthSession.userEmailIsVerified, expectedPersistAuth.userEmailIsVerified) 305 | XCTAssertEqual(testAuthSession.canAuthorize, expectedPersistAuth.canAuthorize) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods', '1.15.2' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.3) 5 | activesupport (7.1.3.4) 6 | base64 7 | bigdecimal 8 | concurrent-ruby (~> 1.0, >= 1.0.2) 9 | connection_pool (>= 2.2.5) 10 | drb 11 | i18n (>= 1.6, < 2) 12 | minitest (>= 5.1) 13 | mutex_m 14 | tzinfo (~> 2.0) 15 | addressable (2.8.7) 16 | public_suffix (>= 2.0.2, < 7.0) 17 | algoliasearch (1.27.5) 18 | httpclient (~> 2.8, >= 2.8.3) 19 | json (>= 1.5.1) 20 | atomos (0.1.3) 21 | base64 (0.2.0) 22 | bigdecimal (3.1.8) 23 | claide (1.0.3) 24 | cocoapods (1.15.2) 25 | addressable (~> 2.6) 26 | claide (>= 1.0.2, < 2.0) 27 | cocoapods-core (= 1.15.2) 28 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 29 | cocoapods-downloader (>= 1.4.0, < 2.0) 30 | cocoapods-plugins (>= 1.0.0, < 2.0) 31 | cocoapods-search (>= 1.0.0, < 2.0) 32 | cocoapods-trunk (>= 1.4.0, < 2.0) 33 | cocoapods-try (>= 1.1.0, < 2.0) 34 | colored2 (~> 3.1) 35 | escape (~> 0.0.4) 36 | fourflusher (>= 2.3.0, < 3.0) 37 | gh_inspector (~> 1.0) 38 | molinillo (~> 0.6.6) 39 | nap (~> 1.0) 40 | ruby-macho (~> 1.4) 41 | xcodeproj (>= 1.19.0, < 2.0) 42 | cocoapods-core (1.15.2) 43 | activesupport (>= 5.0, < 8) 44 | addressable (~> 2.8) 45 | algoliasearch (~> 1.0) 46 | concurrent-ruby (~> 1.1) 47 | fuzzy_match (~> 2.0.4) 48 | nap (~> 1.0) 49 | netrc (~> 0.11) 50 | public_suffix (~> 4.0) 51 | typhoeus (~> 1.0) 52 | cocoapods-deintegrate (1.0.4) 53 | cocoapods-downloader (1.6.3) 54 | cocoapods-plugins (1.0.0) 55 | nap 56 | cocoapods-search (1.0.0) 57 | cocoapods-trunk (1.5.0) 58 | nap (>= 0.8, < 2.0) 59 | netrc (~> 0.11) 60 | cocoapods-try (1.2.0) 61 | colored2 (3.1.2) 62 | concurrent-ruby (1.3.3) 63 | connection_pool (2.4.1) 64 | drb (2.2.1) 65 | escape (0.0.4) 66 | ethon (0.16.0) 67 | ffi (>= 1.15.0) 68 | ffi (1.17.0) 69 | fourflusher (2.3.1) 70 | fuzzy_match (2.0.4) 71 | gh_inspector (1.1.3) 72 | httpclient (2.8.3) 73 | i18n (1.14.5) 74 | concurrent-ruby (~> 1.0) 75 | json (2.7.2) 76 | minitest (5.24.1) 77 | molinillo (0.6.6) 78 | mutex_m (0.2.0) 79 | nanaimo (0.3.0) 80 | nap (1.1.0) 81 | netrc (0.11.0) 82 | public_suffix (4.0.7) 83 | ruby-macho (1.4.0) 84 | typhoeus (1.4.1) 85 | ethon (>= 0.9.0) 86 | tzinfo (2.0.6) 87 | concurrent-ruby (~> 1.0) 88 | xcodeproj (1.19.0) 89 | CFPropertyList (>= 2.3.3, < 4.0) 90 | atomos (~> 0.1.3) 91 | claide (>= 1.0.2, < 2.0) 92 | colored2 (~> 3.1) 93 | nanaimo (~> 0.3.0) 94 | 95 | PLATFORMS 96 | ruby 97 | 98 | DEPENDENCIES 99 | cocoapods (= 1.15.2) 100 | 101 | BUNDLED WITH 102 | 2.5.4 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | // Copyright 2021 Google LLC 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | import PackageDescription 19 | 20 | let package = Package( 21 | name: "GTMAppAuth", 22 | platforms: [ 23 | .macOS(.v10_12), 24 | .iOS(.v12), 25 | .tvOS(.v10), 26 | .watchOS(.v6) 27 | ], 28 | products: [ 29 | .library( 30 | name: "GTMAppAuth", 31 | targets: ["GTMAppAuth"] 32 | ) 33 | ], 34 | dependencies: [ 35 | .package(url: "https://github.com/google/gtm-session-fetcher.git", "3.3.0" ..< "4.0.0"), 36 | .package(url: "https://github.com/openid/AppAuth-iOS.git", "2.0.0" ..< "3.0.0") 37 | ], 38 | targets: [ 39 | .target( 40 | name: "GTMAppAuth", 41 | dependencies: [ 42 | .product(name: "GTMSessionFetcherCore", package: "gtm-session-fetcher"), 43 | .product(name: "AppAuthCore", package: "AppAuth-iOS") 44 | ], 45 | path: "GTMAppAuth/Sources", 46 | resources: [.copy("Resources/PrivacyInfo.xcprivacy")], 47 | linkerSettings: [ 48 | .linkedFramework("Security"), 49 | ] 50 | ), 51 | .target( 52 | name: "TestHelpers", 53 | dependencies: [ 54 | .product(name: "AppAuthCore", package: "AppAuth-iOS"), 55 | "GTMAppAuth" 56 | ], 57 | path: "GTMAppAuth/Tests/Helpers" 58 | ), 59 | .testTarget( 60 | name: "GTMAppAuthTests", 61 | dependencies: [ 62 | "GTMAppAuth", 63 | "TestHelpers" 64 | ], 65 | path: "GTMAppAuth/Tests/Unit" 66 | ), 67 | .testTarget( 68 | name: "swift-objc-interop-tests", 69 | dependencies: [ 70 | .product(name: "AppAuthCore", package: "AppAuth-iOS"), 71 | "GTMAppAuth", 72 | "TestHelpers" 73 | ], 74 | path: "GTMAppAuth/Tests/ObjCIntegration" 75 | ) 76 | ] 77 | ) 78 | --------------------------------------------------------------------------------