├── .eslintignore ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── -everything-else--report.md │ ├── bug-report.md │ └── feature-request.md └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── CapacitorCommunityGenericOAuth2.podspec ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── proguard-rules.pro ├── settings.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── getcapacitor │ │ │ └── community │ │ │ └── genericoauth2 │ │ │ ├── ConfigUtils.java │ │ │ ├── GenericOAuth2Plugin.java │ │ │ ├── OAuth2Options.java │ │ │ ├── OAuth2RefreshTokenOptions.java │ │ │ ├── OAuth2Utils.java │ │ │ ├── ResourceCallResult.java │ │ │ ├── ResourceUrlAsyncTask.java │ │ │ └── handler │ │ │ ├── AccessTokenCallback.java │ │ │ └── OAuth2CustomHandler.java │ └── res │ │ └── .gitkeep │ └── test │ └── java │ └── com │ └── getcapacitor │ └── community │ └── genericoauth2 │ ├── ConfigUtilsTest.java │ └── GenericOAuth2PluginTest.java ├── ios ├── Plugin.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── Plugin.xcscheme │ │ └── PluginTests.xcscheme ├── Plugin.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Plugin │ ├── GenericOAuth2Plugin.h │ ├── GenericOAuth2Plugin.m │ ├── GenericOAuth2Plugin.swift │ ├── Info.plist │ ├── OAuth2CustomHandler.swift │ └── OAuth2SafariDelegate.swift ├── PluginTests │ ├── GenericOAuth2Tests.swift │ └── Info.plist └── Podfile ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── definitions.ts ├── index.ts ├── web-utils.test.ts ├── web-utils.ts └── web.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | I'm happy to accept external contributions to the project in the form of feedback, 5 | bug reports and even better - pull requests 6 | 7 | ## Issues 8 | 9 | Issues are mostly used to track **bugs** and **feature requests** but you can also 10 | ask questions as it's the only place I'm looking at. 11 | 12 | Before reporting a bug or requesting a feature, run a few searches to 13 | see if a similar issue has already been opened and ensure you’re not submitting 14 | a duplicate. 15 | 16 | ### Bugs 17 | * Choose the "Bug Report" template 18 | * Fill in all relevant information, especially 19 | * Describe steps to reproduce 20 | * Full error message if any 21 | * Your code if relevant 22 | 23 | ### Feature Requests 24 | * Choose the "Feature Request" template 25 | * Describe the feature. Be specific 26 | * Explain why I should implement it. 27 | 28 | ## Pull Request Guidelines 29 | * Please check to make sure that there aren't existing pull requests attempting to address the issue mentioned. 30 | * Open a single PR for each subject. 31 | * Develop in a topic branch, not main (feature-name). 32 | * Write a convincing description of your PR and why I should land it. 33 | * Update documentation comments where applicable. 34 | 35 | ### Only touch relevant files 36 | 37 | * Make sure your PR stays focused on a single feature. 38 | * Don't change project configs or any files unrelated to the subject you're working. 39 | * Don't reformat code you don't modify. 40 | 41 | ### Fixing a bug? 42 | * Mention it or create an issue if not exist 43 | * Do not forgot to put [Fix # in your commit message to auto close](https://help.github.com/articles/closing-issues-via-commit-messages/) 44 | 45 | ### Keep your commit history short and clean. 46 | * Keeping the history clean means making one commit per feature. (no fix of your fix) 47 | * I will squash every PR. 48 | 49 | ### Make sure tests pass (if exist) 50 | * Add relevant tests to cover the change. 51 | * Make sure test-suite passes. 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-everything-else--report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '"Everything else" Report' 3 | about: Use this if it's NOT a bug or feature request 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ### Description 15 | 16 | ### Capacitor version: 17 | 19 | 20 | Run `npx cap doctor`: 21 | 22 | ``` 23 | Replace this with the commands output 24 | ``` 25 | 26 | ### Library version: 27 | 28 | 29 | - 3.0.1 30 | - 2.1.0 31 | - 2.0.0 32 | - other: (Please fill in the version you are using.) 33 | 34 | ### OAuth Provider: 35 | 36 | 37 | - Google 38 | - Facebook 39 | - Azure AD (B2C) 40 | - Github 41 | - Other: (Please fill in the provider you are using.) 42 | 43 | ### Your Plugin Configuration 44 | 45 | 46 | ```typescript 47 | { 48 | // Replace this with your plugin configuration 49 | } 50 | ``` 51 | 52 | ### Affected Platform(s): 53 | 54 | 55 | * Android 56 | * Version/API Level: 57 | * Device Model: 58 | * Content of your `AndroidManifest.xml` 59 | ```xml 60 | 61 | ``` 62 | * iOS 63 | * Version/API Level: 64 | * Device Model: 65 | * Content of your `Info.plist` 66 | ```xml 67 | 68 | ``` 69 | * Web 70 | * Browser: 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Template to report bugs. 4 | title: 'Bug: ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ### Capacitor version: 15 | 17 | 18 | Run `npx cap doctor`: 19 | 20 | ``` 21 | Replace this with the commands output 22 | ``` 23 | 24 | ### Library version: 25 | 26 | 27 | - 3.0.1 28 | - 2.1.0 29 | - 2.0.0 30 | - other: (Please fill in the version you are using.) 31 | 32 | ### OAuth Provider: 33 | 34 | 35 | - Google 36 | - Facebook 37 | - Azure AD (B2C) 38 | - Github 39 | - Other: (Please fill in the provider you are using.) 40 | 41 | ### Your Plugin Configuration 42 | 43 | 44 | ```typescript 45 | { 46 | // Replace this with your plugin configuration 47 | } 48 | ``` 49 | 50 | ### Affected Platform(s): 51 | 52 | 53 | * Android 54 | * Version/API Level: 55 | * Device Model: 56 | * Content of your `AndroidManifest.xml` 57 | ```xml 58 | 59 | ``` 60 | * iOS 61 | * Version/API Level: 62 | * Device Model: 63 | * Content of your `Info.plist` 64 | ```xml 65 | 66 | ``` 67 | * Web 68 | * Browser: 69 | 70 | ### Current Behavior 71 | 72 | 73 | 74 | ### Expected Behavior 75 | 76 | 77 | 78 | ### Sample Code or Sample Application Repo 79 | 80 | 81 | 82 | ### Reproduction Steps 83 | 84 | 85 | 86 | ### Other Information 87 | 88 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request a feature addition or change. 4 | title: 'Feat: ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ### Describe the Feature 15 | 16 | 17 | ### Platform(s) Support Requested 18 | 19 | 20 | - Android 21 | - iOS 22 | - Electron 23 | - Web 24 | 25 | ### Describe Preferred Solution 26 | 27 | 28 | ### Describe Alternatives 29 | 30 | 31 | ### Related Code 32 | 33 | 34 | ### Additional Context 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '*.*' 9 | paths-ignore: 10 | - README.md 11 | 12 | jobs: 13 | test-web: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 30 16 | 17 | steps: 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: 16 21 | - uses: actions/checkout@v2 22 | - name: Restore Dependency Cache 23 | uses: actions/cache@v1 24 | with: 25 | path: ~/.npm 26 | key: ${{ runner.OS }}-dependency-cache-${{ hashFiles('**/package.json') }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | # test-ios: 31 | # runs-on: macos-latest 32 | # timeout-minutes: 30 33 | # strategy: 34 | # matrix: 35 | # xcode: 36 | # - /Applications/Xcode_12.4.app 37 | # steps: 38 | # - run: sudo xcode-select --switch ${{ matrix.xcode }} 39 | # - uses: actions/setup-node@v1 40 | # with: 41 | # node-version: 14.x 42 | # - uses: actions/checkout@v2 43 | # - name: Restore Dependency Cache 44 | # uses: actions/cache@v1 45 | # with: 46 | # path: ~/.npm 47 | # key: ${{ runner.OS }}-dependency-cache-${{ hashFiles('**/package.json') }} 48 | # - run: npm install 49 | # - run: npm run verify 50 | # working-directory: ./ios 51 | # test-android: 52 | # runs-on: ubuntu-latest 53 | # timeout-minutes: 30 54 | # steps: 55 | # - name: Checkout latest source 56 | # uses: actions/checkout@v2 57 | # 58 | # - name: Set up JDK 11 59 | # uses: actions/setup-java@v1 60 | # with: 61 | # java-version: 11 62 | # 63 | # - name: Set up Node 64 | # uses: actions/setup-node@v2 65 | # with: 66 | # node-version: 16 67 | # 68 | # - name: Grant execute permission for gradlew 69 | # run: chmod +x gradlew 70 | # working-directory: ./android 71 | # 72 | # - name: Cache .gradle 73 | # uses: actions/cache@v1 74 | # with: 75 | # path: .gradle 76 | # key: ${{ runner.os }}-dotgradle-${{ hashFiles('**/build.gradle') }} 77 | # restore-keys: | 78 | # ${{ runner.os }}-dotgradle- 79 | # 80 | # - name: Cache gradle 81 | # uses: actions/cache@v1 82 | # with: 83 | # path: ~/.gradle 84 | # key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }} 85 | # restore-keys: | 86 | # ${{ runner.os }}-gradle- 87 | # 88 | # - name: Install Capacitor Android dependency 89 | # run: npm ci 90 | # 91 | # - name: Run Tests 92 | # run: ./gradlew test 93 | # working-directory: ./android 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node files 2 | dist 3 | node_modules 4 | 5 | # iOS files 6 | Pods 7 | Podfile.lock 8 | Build 9 | xcuserdata 10 | 11 | # macOS files 12 | .DS_Store 13 | 14 | 15 | 16 | # Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore 17 | 18 | # Built application files 19 | *.apk 20 | *.ap_ 21 | 22 | # Files for the ART/Dalvik VM 23 | *.dex 24 | 25 | # Java class files 26 | *.class 27 | 28 | # Generated files 29 | bin 30 | gen 31 | out 32 | 33 | # Gradle files 34 | .gradle 35 | build 36 | 37 | # Local configuration file (sdk path, etc) 38 | local.properties 39 | 40 | # Proguard folder generated by Eclipse 41 | proguard 42 | 43 | # Log Files 44 | *.log 45 | 46 | # Android Studio Navigation editor temp files 47 | .navigation 48 | 49 | # Android Studio captures folder 50 | captures 51 | 52 | # IntelliJ 53 | *.iml 54 | .idea 55 | 56 | # Keystore files 57 | # Uncomment the following line if you do not want to check your keystore files in. 58 | #*.jks 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | 63 | # Locally published versions 64 | capacitor-community-generic-oauth2-*.tgz 65 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [6.x.x] - 2024 4 | 5 | See [GitHub Releases](https://github.com/capacitor-community/generic-oauth2/releases) for details 6 | 7 | ## [5.0.0] - 2023-09-04 8 | 9 | ### Breaking 10 | * Minimum Capacitor version is **5.0.0**.! [#211](https://github.com/moberwasserlechner/capacitor-oauth2/issues/211) 11 | * Remove web option `windowReplace` as it is deprecated and gives build exceptions. See https://www.w3schools.com/jsref/met_win_open.asp for details. 12 | 13 | ## [4.0.2] - 2023-04-11 14 | 15 | ### Fixed 16 | 17 | * Web: Parse url parameters for search and hash properly [#183](https://github.com/moberwasserlechner/capacitor-oauth2/pull/183), [#182](https://github.com/moberwasserlechner/capacitor-oauth2/issues/182). Thank you, [@jvartanian](https://github.com/jvartanian) 18 | 19 | ## [4.0.1] - 2023-04-11 20 | 21 | ### Fixed 22 | 23 | * Android: Additional `id_token` argument for logout method [#233](https://github.com/moberwasserlechner/capacitor-oauth2/pull/233). Thank you, [@svzi](https://github.com/svzi) 24 | 25 | ### Chore 26 | 27 | * Update dev dependencies 28 | 29 | ## [4.0.0] - 2022-09-18 30 | 31 | ### Fixed 32 | 33 | * Detection of Network Errors when refreshing Tokens [#192](https://github.com/moberwasserlechner/capacitor-oauth2/issues/192) 34 | * Popup blocked in Safari for pkce flow [#216](https://github.com/moberwasserlechner/capacitor-oauth2/issues/216) 35 | 36 | ### Breaking 37 | * Minimum Capacitor version is **4.0.0**.! [#211](https://github.com/moberwasserlechner/capacitor-oauth2/issues/211) 38 | 39 | ## [3.0.1] - 2021-08-11 40 | 41 | ### Docs 42 | * Where to securely save tokens [README](https://github.com/moberwasserlechner/capacitor-oauth2/#where-to-store-access-tokens) entry. [#139](https://github.com/moberwasserlechner/capacitor-oauth2/issues/139) Thank you [@RaphaelWoude](https://github.com/RaphaelWoude) 43 | 44 | ### Changed 45 | * Chore: Use main instead of master branch. [#168](https://github.com/moberwasserlechner/capacitor-oauth2/issues/168) 46 | 47 | ### Fixed 48 | * Android: Use json for responses instead of string. [#171](https://github.com/moberwasserlechner/capacitor-oauth2/issues/171) Thank you [@webflo](https://github.com/webflo) 49 | 50 | ## [3.0.0] - 2021-08-02 51 | 52 | ### Breaking 53 | * Minimum Capacitor version is **3.0.0**. Only this plugin version supports Capacitor `3.x`! [#138](https://github.com/moberwasserlechner/capacitor-oauth2/issues/138) [#140](https://github.com/moberwasserlechner/capacitor-oauth2/pull/140) 54 | 55 | ### Added 56 | * Web: Add a new option `windowReplace` that defaults to undefined. Used in `window.open()` 4th param. 57 | This will fix https://bugs.chromium.org/p/chromium/issues/detail?id=1164959 [#153](https://github.com/moberwasserlechner/capacitor-oauth2/issues/153) 58 | * Web, Android: Add "authorization_response" and "access_token_response" to the result returned to JS. On iOS it is not possible to extract the authorization response because of the used lib. [#154](https://github.com/moberwasserlechner/capacitor-oauth2/issues/154) 59 | * Web, Android: Added `additionalResourceHeaders` to base options 60 | * Web, Android, iOS: Added `logsEnabled` to base options. If enabled extensive logs are written. All logs are prefixed with `I/Capacitor/OAuth2ClientPlugin` across all platforms. 61 | 62 | ### Changed 63 | * Use `window.crypto` if available to generate random strings [#138](https://github.com/moberwasserlechner/capacitor-oauth2/issues/138) [#140](https://github.com/moberwasserlechner/capacitor-oauth2/pull/140) 64 | 65 | ### Fixed 66 | * Web: # in URL causes parser to ignore ? [#132](https://github.com/moberwasserlechner/capacitor-oauth2/issues/132) [#133](https://github.com/moberwasserlechner/capacitor-oauth2/pull/133) 67 | * Android: Fix boolean param inheritance (#162) [#162](https://github.com/moberwasserlechner/capacitor-oauth2/issues/162) 68 | 69 | ## [2.1.0] - 2020-08-27 70 | 71 | ### Added 72 | 73 | * ios: Sign in with Apple. Closes [#45](https://github.com/moberwasserlechner/capacitor-oauth2/issues/45). 74 | The plugin will detect that the iOS 13+ buildin UI is needed, when `authorizationBaseUrl` contains `appleid.apple.com`. 75 | This is needed for other platforms and iOS <=12 anyway. Android, web, iOS <12 are not supported in this release. 76 | 77 | ### Fixed 78 | 79 | * web: Make web flow work if server and client are hosted on same server. Closes [#94](https://github.com/moberwasserlechner/capacitor-oauth2/issues/94). thx [@klot-git](https://github.com/klot-git) 80 | 81 | ### Changed 82 | 83 | * iOS: Upgrade SwiftOAuth2 to head. Closes [#105](https://github.com/moberwasserlechner/capacitor-oauth2/issues/105) 84 | 85 | ## [2.0.0] - 2020-04-20 86 | 87 | ### Breaking 88 | * Core: Capacitor 2.x is new minimum peer dependency. closes #80. 89 | * `responseType` is required. Default values were removed. In favor of configuring anything. closes #86. 90 | * `pkceDisabled` was replaced with `pkceEnabled`, which is NOT enabled by default. If you like to use PKCE set this to true. 91 | * If a flow must not have a `accessTokenEndpoint` but you configured one as base parameter you have to 92 | overwrite it in the according platform sections. `accessTokenEndpoint: ""` see Google example in README. 93 | * Add `redirectUrl` to base parameter and make it overwritable in the platform sections. closes #84. 94 | * Android: `customScheme` replaced by `redirectUrl` 95 | * iOS: `customScheme` replaced by `redirectUrl` 96 | * Additional method argument for `OAuth2CustomHandler#logout`. closes #58 97 | * Android: `activity` as 1st argument 98 | * iOS: `viewController` as 1st argument 99 | 100 | ### Added 101 | * iOS: If the user touches "done" in safari without entering the credentials 102 | the USER_CANCELLED error is sent. closes #71 103 | * Web: Include all url params from the accessToken request if no resourceUrl is present. closes #72. thx [@sanjaywadhwani](https://github.com/sanjaywadhwani) 104 | * Android: Add an alternative to handle the activity result intent. 105 | This is controlled by Android specific parameters `handleResultOnNewIntent` for the alternative and `handleResultOnActivityResult` for the default. closes #52, #55. 106 | 107 | ### Changed 108 | * Android: Allow no resource url and just return every we got until so far. closes #75. thx [@0x4AMiller](https://github.com/0x4AMiller) 109 | * Web, iOS, Android: All base parameters are overwritable in the platform sections. closes #84. 110 | * Restriction to the response type `code` and `token` was removed. Devs can configure anything but are responsible for it as well. closes #86. 111 | 112 | ### Fixed 113 | 114 | * iOS: XCode 11.4 crash on app start. closes #73. thx [@macdja38](https://github.com/macdja38) 115 | 116 | ### Docs 117 | 118 | * CustomHandler Facebook example logout fixed. closes #79. thx [@REPTILEHAUS](https://github.com/REPTILEHAUS) 119 | * Facebook force authentication with FB App. closes #69. thx [@mrbatista](https://github.com/mrbatista) 120 | 121 | ## [1.1.0] - 2020-01-22 122 | ### Changed 123 | - Docs for Facebook if using iOS 13 and Facebook pod 5.x #56 124 | - Align Android behavior to iOS where the additional parameters are not overwritten #57 (thx @maggix) 125 | - Upgrade dev dependencies to Capacitor 1.4.0 126 | 127 | ### Added 128 | - Refresh token feature for iOS and Android #64 (thx @dennisameling) 129 | - Detect when user cancels authentication on web (implicit flow) #25 (thx @michaeltintiuc) 130 | 131 | ## [1.0.1] - 2019-09-19 132 | ### Added 133 | - Add OpenID not supported to README 134 | - Add CHANGELOG file to project 135 | 136 | ### Fixed 137 | - web/pwa: `pkceCodeChallenge` was always `undefined` because promise was not awaited properly #53 (thx @nicksteenstra) 138 | 139 | ## [1.0.0] - 2019-06-26 140 | 141 | ### Added 142 | - Add minimum cap version to installation notice 143 | 144 | ### Changed 145 | - Upgrade to Capacitor 1.0.0 #43,#39 146 | 147 | ### Fixed 148 | - Android: Fix plugin does not send resource url response to app after specific steps #28 149 | - Android: Fix Java compiler error #36 (thx @Anthbs) 150 | - Fix github security error by updating Jest lib 151 | 152 | [Unreleased]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/5.0.0...main 153 | [5.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.2...5.0.0 154 | [4.0.2]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.1...4.0.2 155 | [4.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.0...4.0.1 156 | [4.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/3.0.1...4.0.0 157 | [3.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/3.0.0...3.0.1 158 | [3.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/2.1.0...3.0.0 159 | [2.1.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/2.0.0...2.1.0 160 | [2.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.1.0...2.0.0 161 | [1.1.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.0.1...1.1.0 162 | [1.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.0.0...1.0.1 163 | [1.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/releases/tag/1.0.0 164 | -------------------------------------------------------------------------------- /CapacitorCommunityGenericOAuth2.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = 'CapacitorCommunityGenericOauth2' 7 | s.version = package['version'] 8 | s.summary = package['description'] 9 | s.license = package['license'] 10 | s.homepage = package['repository']['url'] 11 | s.author = package['author'] 12 | s.source = { :git => package['repository']['url'], :tag => s.version.to_s } 13 | s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' 14 | s.ios.deployment_target = '13.0' 15 | s.dependency 'Capacitor' 16 | s.dependency 'OAuthSwift', '2.2.0' 17 | s.swift_version = '5.1' 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Capacitor Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | appAuthVersion = project.hasProperty('appAuthVersion') ? rootProject.ext.appAuthVersion : '0.9.1' 3 | androidxBrowserVersion = project.hasProperty('androidxBrowserVersion') ? rootProject.ext.androidxBrowserVersion : '1.7.0' 4 | junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' 5 | commonsIoVersion = project.hasProperty('commonsIoVersion') ? rootProject.ext.commonsIoVersion : '2.10.0' 6 | androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1' 7 | junit5Version = project.hasProperty('junit5Version') ? rootProject.ext.junit5Version : '5.7.2' 8 | androidJunit5Version = project.hasProperty('androidJunit5Version') ? rootProject.ext.androidJunit5Version : '1.2.2' 9 | androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1' 10 | } 11 | 12 | buildscript { 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | dependencies { 18 | classpath 'com.android.tools.build:gradle:8.2.1' 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | namespace "com.getcapacitor.community.genericoauth2" 26 | compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34 27 | defaultConfig { 28 | minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22 29 | targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34 30 | versionCode 1 31 | versionName "1.0" 32 | // 1) Make sure to use the AndroidJUnitRunner, or a subclass of it. This requires a dependency on androidx.test:runner, too! 33 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 34 | // 2) Connect JUnit 5 to the runner 35 | // testInstrumentationRunnerArgument("runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder") 36 | } 37 | buildTypes { 38 | release { 39 | minifyEnabled false 40 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 41 | } 42 | } 43 | lintOptions { 44 | abortOnError false 45 | } 46 | 47 | compileOptions { 48 | sourceCompatibility JavaVersion.VERSION_17 49 | targetCompatibility JavaVersion.VERSION_17 50 | } 51 | 52 | unitTestVariants.all { 53 | it.mergedFlavor.manifestPlaceholders += [ 54 | appAuthRedirectScheme: "com.getcapacitor.community.genericoauth2app" 55 | ] 56 | } 57 | 58 | // testOptions { 59 | // unitTests { 60 | // all { 61 | // include 'com.getcapacitor.community.genericoauth2' 62 | // } 63 | // } 64 | // } 65 | } 66 | 67 | repositories { 68 | google() 69 | mavenCentral() 70 | } 71 | 72 | dependencies { 73 | implementation fileTree(dir: 'libs', include: ['*.jar']) 74 | implementation project(':capacitor-android') 75 | 76 | implementation "androidx.browser:browser:$androidxBrowserVersion" 77 | implementation "net.openid:appauth:$appAuthVersion" 78 | implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" 79 | 80 | // 4) Jupiter API & Test Runner, if you don't have it already 81 | testImplementation("org.junit.jupiter:junit-jupiter-params:${junit5Version}") 82 | testImplementation("org.junit.jupiter:junit-jupiter-api:${junit5Version}") { 83 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 84 | } 85 | testImplementation "commons-io:commons-io:$commonsIoVersion" 86 | } 87 | 88 | // ############### 89 | // ### AppAuth ### 90 | // ############### 91 | 92 | android.defaultConfig.manifestPlaceholders = [ 93 | 'appAuthRedirectScheme': 'com.getcapacitor.community.genericoauth2app' 94 | ] 95 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capacitor-community/generic-oauth2/2273cfb322967b78f0297100e2383f5628d459bd/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 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 | # https://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 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | if ! command -v java >/dev/null 2>&1 134 | then 135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 136 | 137 | Please set the JAVA_HOME variable in your environment to match the 138 | location of your Java installation." 139 | fi 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | 201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 203 | 204 | # Collect all arguments for the java command; 205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 206 | # shell script including quotes and variable substitutions, so put them in 207 | # double quotes to make sure that they get re-expanded; and 208 | # * put everything else in single quotes, so that it's not re-expanded. 209 | 210 | set -- \ 211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 212 | -classpath "$CLASSPATH" \ 213 | org.gradle.wrapper.GradleWrapperMain \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':capacitor-android' 2 | project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | import com.getcapacitor.JSObject; 4 | import java.util.HashMap; 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | import java.util.Random; 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | 11 | public abstract class ConfigUtils { 12 | 13 | public static String getParamString(JSObject data, String key) { 14 | return getParam(String.class, data, key); 15 | } 16 | 17 | public static T getParam(Class clazz, JSObject data, String key) { 18 | return getParam(clazz, data, key, null); 19 | } 20 | 21 | public static T getParam(Class clazz, JSObject data, String key, T defaultValue) { 22 | String k = getDeepestKey(key); 23 | if (k != null) { 24 | try { 25 | Object value = null; 26 | JSONObject o = getDeepestObject(data, key); 27 | 28 | // #109 29 | if (o.has(k)) { 30 | if (clazz.isAssignableFrom(String.class)) { 31 | value = o.getString(k); 32 | } else if (clazz.isAssignableFrom(Boolean.class)) { 33 | value = o.optBoolean(k); 34 | } else if (clazz.isAssignableFrom(Double.class)) { 35 | value = o.getDouble(k); 36 | } else if (clazz.isAssignableFrom(Integer.class)) { 37 | value = o.getInt(k); 38 | } else if (clazz.isAssignableFrom(Long.class)) { 39 | value = o.getLong(k); 40 | } else if (clazz.isAssignableFrom(Float.class)) { 41 | Double doubleValue = o.getDouble(k); 42 | value = doubleValue.floatValue(); 43 | } else if (clazz.isAssignableFrom(Integer.class)) { 44 | value = o.getInt(k); 45 | } 46 | } 47 | if (value == null) { 48 | return defaultValue; 49 | } 50 | return (T) value; 51 | } catch (Exception ignore) {} 52 | } 53 | return defaultValue; 54 | } 55 | 56 | public static Map getParamMap(JSObject data, String key) { 57 | Map map = new HashMap<>(); 58 | String k = getDeepestKey(key); 59 | if (k != null) { 60 | try { 61 | JSONObject o = getDeepestObject(data, key); 62 | JSONObject jsonObject = o.getJSONObject(k); 63 | Iterator keys = jsonObject.keys(); 64 | while (keys.hasNext()) { 65 | String mapKey = keys.next(); 66 | if (mapKey != null && mapKey.trim().length() > 0) { 67 | try { 68 | String mapValue = jsonObject.getString(mapKey); 69 | map.put(mapKey, mapValue); 70 | } catch (JSONException ignore) {} 71 | } 72 | } 73 | } catch (Exception ignore) {} 74 | } 75 | return map; 76 | } 77 | 78 | public static String getDeepestKey(String key) { 79 | String[] parts = key.split("\\."); 80 | if (parts.length > 0) { 81 | return parts[parts.length - 1]; 82 | } 83 | return null; 84 | } 85 | 86 | public static JSObject getDeepestObject(JSObject o, String key) { 87 | // Split on periods 88 | String[] parts = key.split("\\."); 89 | // Search until the second to last part of the key 90 | for (int i = 0; i < parts.length - 1; i++) { 91 | String k = parts[i]; 92 | o = o.getJSObject(k); 93 | } 94 | return o; 95 | } 96 | 97 | public static T getOverwrittenAndroidParam(Class clazz, JSObject data, String key) { 98 | T baseParam = getParam(clazz, data, key); 99 | T androidParam = getParam(clazz, data, "android." + key); 100 | if (androidParam != null) { 101 | baseParam = androidParam; 102 | } 103 | return baseParam; 104 | } 105 | 106 | public static Map getOverwrittenAndroidParamMap(JSObject data, String key) { 107 | Map baseParam = getParamMap(data, key); 108 | Map androidParam = getParamMap(data, "android." + key); 109 | Map mergedParam = new HashMap<>(baseParam); 110 | mergedParam.putAll(androidParam); 111 | return mergedParam; 112 | } 113 | 114 | public static String getRandomString(int len) { 115 | char[] ch = { 116 | '0', 117 | '1', 118 | '2', 119 | '3', 120 | '4', 121 | '5', 122 | '6', 123 | '7', 124 | '8', 125 | '9', 126 | 'A', 127 | 'B', 128 | 'C', 129 | 'D', 130 | 'E', 131 | 'F', 132 | 'G', 133 | 'H', 134 | 'I', 135 | 'J', 136 | 'K', 137 | 'L', 138 | 'M', 139 | 'N', 140 | 'O', 141 | 'P', 142 | 'Q', 143 | 'R', 144 | 'S', 145 | 'T', 146 | 'U', 147 | 'V', 148 | 'W', 149 | 'X', 150 | 'Y', 151 | 'Z', 152 | 'a', 153 | 'b', 154 | 'c', 155 | 'd', 156 | 'e', 157 | 'f', 158 | 'g', 159 | 'h', 160 | 'i', 161 | 'j', 162 | 'k', 163 | 'l', 164 | 'm', 165 | 'n', 166 | 'o', 167 | 'p', 168 | 'q', 169 | 'r', 170 | 's', 171 | 't', 172 | 'u', 173 | 'v', 174 | 'w', 175 | 'x', 176 | 'y', 177 | 'z' 178 | }; 179 | 180 | char[] c = new char[len]; 181 | Random random = new Random(); 182 | for (int i = 0; i < len; i++) { 183 | c[i] = ch[random.nextInt(ch.length)]; 184 | } 185 | return new String(c); 186 | } 187 | 188 | public static String trimToNull(String value) { 189 | if (value != null && value.trim().length() == 0) { 190 | return null; 191 | } 192 | return value; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | import android.app.Activity; 4 | import android.content.ActivityNotFoundException; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.AsyncTask; 8 | import android.util.Log; 9 | import androidx.activity.result.ActivityResult; 10 | import com.getcapacitor.JSObject; 11 | import com.getcapacitor.Plugin; 12 | import com.getcapacitor.PluginCall; 13 | import com.getcapacitor.PluginMethod; 14 | import com.getcapacitor.annotation.ActivityCallback; 15 | import com.getcapacitor.annotation.CapacitorPlugin; 16 | import com.getcapacitor.community.genericoauth2.handler.AccessTokenCallback; 17 | import com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler; 18 | import java.util.Map; 19 | import net.openid.appauth.AuthState; 20 | import net.openid.appauth.AuthorizationException; 21 | import net.openid.appauth.AuthorizationRequest; 22 | import net.openid.appauth.AuthorizationResponse; 23 | import net.openid.appauth.AuthorizationService; 24 | import net.openid.appauth.AuthorizationServiceConfiguration; 25 | import net.openid.appauth.EndSessionRequest; 26 | import net.openid.appauth.EndSessionResponse; 27 | import net.openid.appauth.GrantTypeValues; 28 | import net.openid.appauth.TokenRequest; 29 | import net.openid.appauth.TokenResponse; 30 | import org.json.JSONException; 31 | 32 | @CapacitorPlugin(name = "GenericOAuth2") 33 | public class GenericOAuth2Plugin extends Plugin { 34 | 35 | private static final String PARAM_APP_ID = "appId"; 36 | private static final String PARAM_AUTHORIZATION_BASE_URL = "authorizationBaseUrl"; 37 | private static final String PARAM_RESPONSE_TYPE = "responseType"; 38 | private static final String PARAM_REDIRECT_URL = "redirectUrl"; 39 | private static final String PARAM_SCOPE = "scope"; 40 | private static final String PARAM_STATE = "state"; 41 | 42 | private static final String PARAM_ACCESS_TOKEN_ENDPOINT = "accessTokenEndpoint"; 43 | private static final String PARAM_PKCE_ENABLED = "pkceEnabled"; 44 | private static final String PARAM_RESOURCE_URL = "resourceUrl"; 45 | private static final String PARAM_ADDITIONAL_RESOURCE_HEADERS = "additionalResourceHeaders"; 46 | private static final String PARAM_ADDITIONAL_PARAMETERS = "additionalParameters"; 47 | private static final String PARAM_ANDROID_CUSTOM_HANDLER_CLASS = "android.customHandlerClass"; 48 | // Activity result handling 49 | private static final String PARAM_ANDROID_HANDLE_RESULT_ON_NEW_INTENT = "android.handleResultOnNewIntent"; 50 | private static final String PARAM_ANDROID_HANDLE_RESULT_ON_ACTIVITY_RESULT = "android.handleResultOnActivityResult"; 51 | 52 | // Refresh token params 53 | private static final String PARAM_REFRESH_TOKEN = "refreshToken"; 54 | 55 | // open id params 56 | private static final String PARAM_DISPLAY = "display"; 57 | private static final String PARAM_LOGIN_HINT = "login_hint"; 58 | private static final String PARAM_PROMPT = "prompt"; 59 | private static final String PARAM_RESPONSE_MODE = "response_mode"; 60 | private static final String PARAM_LOGS_ENABLED = "logsEnabled"; 61 | 62 | private static final String PARAM_LOGOUT_URL = "logoutUrl"; 63 | private static final String PARAM_ID_TOKEN = "id_token"; 64 | 65 | private static final String USER_CANCELLED = "USER_CANCELLED"; 66 | 67 | private static final String ERR_PARAM_NO_APP_ID = "ERR_PARAM_NO_APP_ID"; 68 | private static final String ERR_PARAM_NO_AUTHORIZATION_BASE_URL = "ERR_PARAM_NO_AUTHORIZATION_BASE_URL"; 69 | private static final String ERR_PARAM_NO_REDIRECT_URL = "ERR_PARAM_NO_REDIRECT_URL"; 70 | private static final String ERR_PARAM_NO_RESPONSE_TYPE = "ERR_PARAM_NO_RESPONSE_TYPE"; 71 | 72 | private static final String ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT = "ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT"; 73 | private static final String ERR_PARAM_NO_REFRESH_TOKEN = "ERR_PARAM_NO_REFRESH_TOKEN"; 74 | 75 | private static final String ERR_AUTHORIZATION_FAILED = "ERR_AUTHORIZATION_FAILED"; 76 | private static final String ERR_NO_ACCESS_TOKEN = "ERR_NO_ACCESS_TOKEN"; 77 | private static final String ERR_ANDROID_NO_BROWSER = "ERR_ANDROID_NO_BROWSER"; 78 | private static final String ERR_ANDROID_RESULT_NULL = "ERR_ANDROID_NO_INTENT"; 79 | 80 | private static final String ERR_CUSTOM_HANDLER_LOGIN = "ERR_CUSTOM_HANDLER_LOGIN"; 81 | private static final String ERR_CUSTOM_HANDLER_LOGOUT = "ERR_CUSTOM_HANDLER_LOGOUT"; 82 | 83 | private static final String ERR_GENERAL = "ERR_GENERAL"; 84 | private static final String ERR_STATES_NOT_MATCH = "ERR_STATES_NOT_MATCH"; 85 | private static final String ERR_NO_AUTHORIZATION_CODE = "ERR_NO_AUTHORIZATION_CODE"; 86 | 87 | private OAuth2Options oauth2Options; 88 | private AuthorizationService authService; 89 | private AuthState authState; 90 | private String callbackId; 91 | 92 | public GenericOAuth2Plugin() {} 93 | 94 | @PluginMethod 95 | public void refreshToken(final PluginCall call) { 96 | disposeAuthService(); 97 | OAuth2RefreshTokenOptions oAuth2RefreshTokenOptions = buildRefreshTokenOptions(call.getData()); 98 | 99 | if (oAuth2RefreshTokenOptions.getAppId() == null) { 100 | call.reject(ERR_PARAM_NO_APP_ID); 101 | return; 102 | } 103 | 104 | if (oAuth2RefreshTokenOptions.getAccessTokenEndpoint() == null) { 105 | call.reject(ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT); 106 | return; 107 | } 108 | 109 | if (oAuth2RefreshTokenOptions.getRefreshToken() == null) { 110 | call.reject(ERR_PARAM_NO_REFRESH_TOKEN); 111 | return; 112 | } 113 | 114 | this.authService = new AuthorizationService(getContext()); 115 | 116 | AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration( 117 | Uri.parse(""), 118 | Uri.parse(oAuth2RefreshTokenOptions.getAccessTokenEndpoint()) 119 | ); 120 | 121 | if (this.authState == null) { 122 | this.authState = new AuthState(config); 123 | } 124 | 125 | TokenRequest tokenRequest = new TokenRequest.Builder(config, oAuth2RefreshTokenOptions.getAppId()) 126 | .setGrantType(GrantTypeValues.REFRESH_TOKEN) 127 | .setScope(oAuth2RefreshTokenOptions.getScope()) 128 | .setRefreshToken(oAuth2RefreshTokenOptions.getRefreshToken()) 129 | .build(); 130 | 131 | this.authService.performTokenRequest( 132 | tokenRequest, 133 | (response1, ex) -> { 134 | this.authState.update(response1, ex); 135 | if (ex != null) { 136 | String message = ex.error != null ? ex.error : ERR_GENERAL; 137 | call.reject(message, String.valueOf(ex.code), ex); 138 | } else { 139 | if (response1 != null) { 140 | try { 141 | JSObject json = new JSObject(response1.jsonSerializeString()); 142 | call.resolve(json); 143 | } catch (JSONException e) { 144 | call.reject(ERR_GENERAL, e); 145 | } 146 | } else { 147 | call.reject(ERR_NO_ACCESS_TOKEN); 148 | } 149 | } 150 | } 151 | ); 152 | } 153 | 154 | @PluginMethod 155 | public void authenticate(final PluginCall call) { 156 | this.callbackId = call.getCallbackId(); 157 | disposeAuthService(); 158 | oauth2Options = buildAuthenticateOptions(call.getData()); 159 | if (oauth2Options.getCustomHandlerClass() != null) { 160 | if (oauth2Options.isLogsEnabled()) { 161 | Log.i(getLogTag(), "Entering custom handler: " + oauth2Options.getCustomHandlerClass().getClass().getName()); 162 | } 163 | try { 164 | Class handlerClass = (Class) Class.forName(oauth2Options.getCustomHandlerClass()); 165 | OAuth2CustomHandler handler = handlerClass.newInstance(); 166 | handler.getAccessToken( 167 | getActivity(), 168 | call, 169 | new AccessTokenCallback() { 170 | @Override 171 | public void onSuccess(String accessToken) { 172 | new ResourceUrlAsyncTask(call, oauth2Options, getLogTag(), null, null).execute(accessToken); 173 | } 174 | 175 | @Override 176 | public void onCancel() { 177 | call.reject(USER_CANCELLED); 178 | } 179 | 180 | @Override 181 | public void onError(Exception error) { 182 | call.reject(ERR_CUSTOM_HANDLER_LOGIN, error); 183 | } 184 | } 185 | ); 186 | } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { 187 | call.reject(ERR_CUSTOM_HANDLER_LOGIN, e); 188 | } catch (Exception e) { 189 | call.reject(ERR_GENERAL, e); 190 | } 191 | } else { 192 | // ################################### 193 | // ### Validate required parameter ### 194 | // ################################### 195 | 196 | if (oauth2Options.getAppId() == null) { 197 | call.reject(ERR_PARAM_NO_APP_ID); 198 | return; 199 | } 200 | 201 | if (oauth2Options.getAuthorizationBaseUrl() == null) { 202 | call.reject(ERR_PARAM_NO_AUTHORIZATION_BASE_URL); 203 | return; 204 | } 205 | 206 | if (oauth2Options.getResponseType() == null) { 207 | call.reject(ERR_PARAM_NO_RESPONSE_TYPE); 208 | return; 209 | } 210 | 211 | if (oauth2Options.getRedirectUrl() == null) { 212 | call.reject(ERR_PARAM_NO_REDIRECT_URL); 213 | return; 214 | } 215 | 216 | // ### Configure 217 | 218 | Uri authorizationUri = Uri.parse(oauth2Options.getAuthorizationBaseUrl()); 219 | Uri accessTokenUri; 220 | if (oauth2Options.getAccessTokenEndpoint() != null) { 221 | accessTokenUri = Uri.parse(oauth2Options.getAccessTokenEndpoint()); 222 | } else { 223 | // appAuth does not allow to be the accessTokenUri empty although it is not used unit performTokenRequest 224 | accessTokenUri = authorizationUri; 225 | } 226 | 227 | AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(authorizationUri, accessTokenUri); 228 | 229 | if (this.authState == null) { 230 | this.authState = new AuthState(config); 231 | } 232 | 233 | AuthorizationRequest.Builder builder = new AuthorizationRequest.Builder( 234 | config, 235 | oauth2Options.getAppId(), 236 | oauth2Options.getResponseType(), 237 | Uri.parse(oauth2Options.getRedirectUrl()) 238 | ); 239 | 240 | // app auth always uses a state 241 | if (oauth2Options.getState() != null) { 242 | builder.setState(oauth2Options.getState()); 243 | } 244 | builder.setScope(oauth2Options.getScope()); 245 | if (oauth2Options.isPkceEnabled()) { 246 | builder.setCodeVerifier(oauth2Options.getPkceCodeVerifier()); 247 | } else { 248 | builder.setCodeVerifier(null); 249 | } 250 | if (oauth2Options.getPrompt() != null) { 251 | builder.setPrompt(oauth2Options.getPrompt()); 252 | } 253 | if (oauth2Options.getLoginHint() != null) { 254 | builder.setLoginHint(oauth2Options.getLoginHint()); 255 | } 256 | if (oauth2Options.getResponseMode() != null) { 257 | builder.setResponseMode(oauth2Options.getResponseMode()); 258 | } 259 | if (oauth2Options.getDisplay() != null) { 260 | builder.setDisplay(oauth2Options.getDisplay()); 261 | } 262 | 263 | if (oauth2Options.getAdditionalParameters() != null) { 264 | try { 265 | builder.setAdditionalParameters(oauth2Options.getAdditionalParameters()); 266 | } catch (IllegalArgumentException e) { 267 | // ignore all additional parameter on error 268 | Log.e(getLogTag(), "Additional parameter error", e); 269 | } 270 | } 271 | 272 | AuthorizationRequest req = builder.build(); 273 | 274 | this.authService = new AuthorizationService(getContext()); 275 | try { 276 | Intent authIntent = this.authService.getAuthorizationRequestIntent(req); 277 | this.bridge.saveCall(call); 278 | startActivityForResult(call, authIntent, "handleIntentResult"); 279 | } catch (ActivityNotFoundException e) { 280 | call.reject(ERR_ANDROID_NO_BROWSER, e); 281 | } catch (Exception e) { 282 | Log.e(getLogTag(), "Unexpected exception on open browser for authorization request!"); 283 | call.reject(ERR_GENERAL, e); 284 | } 285 | } 286 | } 287 | 288 | @PluginMethod 289 | public void logout(final PluginCall call) { 290 | String customHandlerClassname = ConfigUtils.getParam(String.class, call.getData(), PARAM_ANDROID_CUSTOM_HANDLER_CLASS); 291 | if (customHandlerClassname != null && customHandlerClassname.length() > 0) { 292 | try { 293 | Class handlerClass = (Class) Class.forName(customHandlerClassname); 294 | OAuth2CustomHandler handler = handlerClass.newInstance(); 295 | boolean successful = handler.logout(getActivity(), call); 296 | if (successful) { 297 | call.resolve(); 298 | } else { 299 | call.reject(ERR_CUSTOM_HANDLER_LOGOUT); 300 | } 301 | } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { 302 | call.reject(ERR_CUSTOM_HANDLER_LOGOUT, e); 303 | } catch (Exception e) { 304 | call.reject(ERR_GENERAL, e); 305 | } 306 | } else { 307 | String idToken = ConfigUtils.getParam(String.class, call.getData(), PARAM_ID_TOKEN); 308 | if (idToken == null) { 309 | this.disposeAuthService(); 310 | this.discardAuthState(); 311 | call.resolve(); 312 | return; 313 | } 314 | 315 | oauth2Options = buildAuthenticateOptions(call.getData()); 316 | 317 | Uri authorizationUri = Uri.parse(oauth2Options.getAuthorizationBaseUrl()); 318 | Uri accessTokenUri; 319 | if (oauth2Options.getAccessTokenEndpoint() != null) { 320 | accessTokenUri = Uri.parse(oauth2Options.getAccessTokenEndpoint()); 321 | } else { 322 | // appAuth does not allow to be the accessTokenUri empty although it is not used unit performTokenRequest 323 | accessTokenUri = authorizationUri; 324 | } 325 | Uri logoutUri = Uri.parse(oauth2Options.getLogoutUrl()); 326 | 327 | AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(authorizationUri, accessTokenUri); 328 | 329 | EndSessionRequest endSessionRequest = new EndSessionRequest.Builder(config) 330 | .setIdTokenHint(idToken) 331 | .setPostLogoutRedirectUri(logoutUri) 332 | .build(); 333 | 334 | this.authService = new AuthorizationService(getContext()); 335 | 336 | try { 337 | Intent endSessionIntent = authService.getEndSessionRequestIntent(endSessionRequest); 338 | this.bridge.saveCall(call); 339 | startActivityForResult(call, endSessionIntent, "handleEndSessionIntentResult"); 340 | } catch (ActivityNotFoundException e) { 341 | call.reject(ERR_ANDROID_NO_BROWSER, e); 342 | } catch (Exception e) { 343 | Log.e(getLogTag(), "Unexpected exception on open browser for logout request!"); 344 | call.reject(ERR_GENERAL, e); 345 | } 346 | } 347 | } 348 | 349 | @Override 350 | protected void handleOnNewIntent(Intent intent) { 351 | // this is a experimental hook and only usable if the android system kills the app between 352 | if (this.oauth2Options != null && this.oauth2Options.isHandleResultOnNewIntent()) { 353 | // with this I have no way to check if this intent is for this plugin 354 | PluginCall savedCall = this.bridge.getSavedCall(this.callbackId); 355 | if (savedCall == null) { 356 | return; 357 | } 358 | handleAuthorizationRequestActivity(intent, savedCall); 359 | } 360 | } 361 | 362 | @ActivityCallback 363 | private void handleIntentResult(PluginCall call, ActivityResult result) { 364 | if (this.oauth2Options != null && this.oauth2Options.isHandleResultOnActivityResult()) { 365 | if (result.getResultCode() == Activity.RESULT_CANCELED) { 366 | call.reject(USER_CANCELLED); 367 | } else { 368 | handleAuthorizationRequestActivity(result.getData(), call); 369 | } 370 | } 371 | } 372 | 373 | @ActivityCallback 374 | private void handleEndSessionIntentResult(PluginCall call, ActivityResult result) { 375 | if (result.getResultCode() == Activity.RESULT_CANCELED) { 376 | call.reject(USER_CANCELLED); 377 | } else { 378 | if (result.getData() != null) { 379 | try { 380 | EndSessionResponse resp = EndSessionResponse.fromIntent(result.getData()); 381 | JSObject json = new JSObject(resp.jsonSerializeString()); 382 | 383 | this.disposeAuthService(); 384 | this.discardAuthState(); 385 | 386 | call.resolve(json); 387 | } catch (Exception e) { 388 | Log.e(getLogTag(), "Unexpected exception on handling result for logout request!"); 389 | call.reject(ERR_GENERAL, e); 390 | return; 391 | } 392 | } 393 | } 394 | } 395 | 396 | void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) { 397 | // there are valid situation when the Intent is null, but 398 | if (intent != null) { 399 | AuthorizationResponse authorizationResponse; 400 | AuthorizationException error; 401 | try { 402 | authorizationResponse = AuthorizationResponse.fromIntent(intent); 403 | error = AuthorizationException.fromIntent(intent); 404 | this.authState.update(authorizationResponse, error); 405 | } catch (Exception e) { 406 | savedCall.reject(ERR_GENERAL, e); 407 | return; 408 | } 409 | 410 | if (error != null) { 411 | if (error.code == AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW.code) { 412 | savedCall.reject(USER_CANCELLED); 413 | } else if (error.code == AuthorizationException.AuthorizationRequestErrors.STATE_MISMATCH.code) { 414 | if (oauth2Options.isLogsEnabled()) { 415 | Log.i(getLogTag(), "State from web options: " + oauth2Options.getState()); 416 | if (authorizationResponse != null) { 417 | Log.i(getLogTag(), "State returned from provider: " + authorizationResponse.state); 418 | } 419 | } 420 | savedCall.reject(ERR_STATES_NOT_MATCH); 421 | } else { 422 | savedCall.reject(ERR_GENERAL, error); 423 | } 424 | return; 425 | } 426 | 427 | // this response may contain the authorizationCode but also idToken and accessToken depending on the flow chosen by responseType 428 | if (authorizationResponse != null) { 429 | if (oauth2Options.isLogsEnabled()) { 430 | Log.i(getLogTag(), "Authorization response:\n" + authorizationResponse.jsonSerializeString()); 431 | } 432 | // if there is a tokenEndpoint configured try to get the accessToken from it. 433 | // it might be already in the authorizationResponse but tokenEndpoint might deliver other tokens. 434 | if (oauth2Options.getAccessTokenEndpoint() != null) { 435 | this.authService = new AuthorizationService(getContext()); 436 | TokenRequest tokenExchangeRequest; 437 | try { 438 | tokenExchangeRequest = authorizationResponse.createTokenExchangeRequest(); 439 | this.authService.performTokenRequest( 440 | tokenExchangeRequest, 441 | (accessTokenResponse, exception) -> { 442 | authState.update(accessTokenResponse, exception); 443 | if (exception != null) { 444 | savedCall.reject(ERR_AUTHORIZATION_FAILED, String.valueOf(exception.code), exception); 445 | } else { 446 | if (accessTokenResponse != null) { 447 | if (oauth2Options.isLogsEnabled()) { 448 | Log.i(getLogTag(), "Access token response:\n" + accessTokenResponse.jsonSerializeString()); 449 | } 450 | authState.performActionWithFreshTokens( 451 | authService, 452 | (accessToken, idToken, ex1) -> { 453 | AsyncTask asyncTask = new ResourceUrlAsyncTask( 454 | savedCall, 455 | oauth2Options, 456 | getLogTag(), 457 | authorizationResponse, 458 | accessTokenResponse 459 | ); 460 | asyncTask.execute(accessToken); 461 | } 462 | ); 463 | } else { 464 | resolveAuthorizationResponse(savedCall, authorizationResponse); 465 | } 466 | } 467 | } 468 | ); 469 | } catch (Exception e) { 470 | savedCall.reject(ERR_NO_AUTHORIZATION_CODE, e); 471 | } 472 | } else { 473 | resolveAuthorizationResponse(savedCall, authorizationResponse); 474 | } 475 | } else { 476 | savedCall.reject(ERR_NO_AUTHORIZATION_CODE); 477 | } 478 | } else { 479 | // the intent is null because the provider send the redirect to the server, which would be valid 480 | // the intent is null because the plugin user configured sth wrong incl. 481 | // the provider does not support redirecting to a android app, which would be invalid 482 | savedCall.reject(ERR_ANDROID_RESULT_NULL); 483 | } 484 | } 485 | 486 | private void resolveAuthorizationResponse(PluginCall savedCall, AuthorizationResponse authorizationResponse) { 487 | JSObject json = new JSObject(); 488 | OAuth2Utils.assignResponses(json, null, authorizationResponse, null); 489 | savedCall.resolve(json); 490 | } 491 | 492 | OAuth2Options buildAuthenticateOptions(JSObject callData) { 493 | OAuth2Options o = new OAuth2Options(); 494 | // required 495 | o.setAppId(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_APP_ID))); 496 | o.setAuthorizationBaseUrl( 497 | ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_AUTHORIZATION_BASE_URL)) 498 | ); 499 | o.setResponseType(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RESPONSE_TYPE))); 500 | o.setRedirectUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REDIRECT_URL))); 501 | 502 | // optional 503 | Boolean logsEnabled = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, callData, PARAM_LOGS_ENABLED); 504 | o.setLogsEnabled(logsEnabled != null && logsEnabled); 505 | o.setResourceUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RESOURCE_URL))); 506 | o.setAccessTokenEndpoint( 507 | ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT)) 508 | ); 509 | Boolean pkceEnabledObj = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, callData, PARAM_PKCE_ENABLED); 510 | o.setPkceEnabled(pkceEnabledObj != null && pkceEnabledObj); 511 | if (o.isPkceEnabled()) { 512 | o.setPkceCodeVerifier(ConfigUtils.getRandomString(64)); 513 | } 514 | 515 | o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE))); 516 | o.setState(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_STATE))); 517 | if (o.getState() == null) { 518 | o.setState(ConfigUtils.getRandomString(20)); 519 | } 520 | 521 | Map additionalParameters = ConfigUtils.getOverwrittenAndroidParamMap(callData, PARAM_ADDITIONAL_PARAMETERS); 522 | if (!additionalParameters.isEmpty()) { 523 | for (Map.Entry entry : additionalParameters.entrySet()) { 524 | String key = entry.getKey(); 525 | if (PARAM_DISPLAY.equals(key)) { 526 | o.setDisplay(entry.getValue()); 527 | } else if (PARAM_LOGIN_HINT.equals(key)) { 528 | o.setLoginHint(entry.getValue()); 529 | } else if (PARAM_PROMPT.equals(key)) { 530 | o.setPrompt(entry.getValue()); 531 | } else if (PARAM_RESPONSE_MODE.equals(key)) { 532 | o.setResponseMode(entry.getValue()); 533 | } else { 534 | o.addAdditionalParameter(key, entry.getValue()); 535 | } 536 | } 537 | } 538 | o.setAdditionalResourceHeaders(ConfigUtils.getOverwrittenAndroidParamMap(callData, PARAM_ADDITIONAL_RESOURCE_HEADERS)); 539 | // android only 540 | o.setCustomHandlerClass(ConfigUtils.trimToNull(ConfigUtils.getParamString(callData, PARAM_ANDROID_CUSTOM_HANDLER_CLASS))); 541 | o.setHandleResultOnNewIntent(ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_NEW_INTENT, false)); 542 | o.setHandleResultOnActivityResult( 543 | ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_ACTIVITY_RESULT, false) 544 | ); 545 | if (!o.isHandleResultOnNewIntent() && !o.isHandleResultOnActivityResult()) { 546 | o.setHandleResultOnActivityResult(true); 547 | } 548 | return o; 549 | } 550 | 551 | OAuth2RefreshTokenOptions buildRefreshTokenOptions(JSObject callData) { 552 | OAuth2RefreshTokenOptions o = new OAuth2RefreshTokenOptions(); 553 | o.setAppId(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_APP_ID))); 554 | o.setAccessTokenEndpoint( 555 | ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT)) 556 | ); 557 | o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE))); 558 | o.setRefreshToken(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REFRESH_TOKEN))); 559 | return o; 560 | } 561 | 562 | @Override 563 | protected void handleOnStop() { 564 | super.handleOnStop(); 565 | disposeAuthService(); 566 | } 567 | 568 | private void disposeAuthService() { 569 | if (authService != null) { 570 | authService.dispose(); 571 | authService = null; 572 | } 573 | } 574 | 575 | private void discardAuthState() { 576 | if (this.authState != null) { 577 | this.authState = null; 578 | } 579 | } 580 | } 581 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class OAuth2Options { 7 | 8 | // required 9 | private String appId; 10 | private String authorizationBaseUrl; 11 | private String responseType; 12 | private String redirectUrl; 13 | 14 | private String scope; 15 | private String state; 16 | 17 | private String accessTokenEndpoint; 18 | private String resourceUrl; 19 | private Map additionalResourceHeaders; 20 | 21 | private boolean pkceEnabled; 22 | private boolean logsEnabled; 23 | private String pkceCodeVerifier; 24 | private Map additionalParameters; 25 | 26 | private String customHandlerClass; 27 | // Activity result handling 28 | private boolean handleResultOnNewIntent; 29 | private boolean handleResultOnActivityResult = true; 30 | 31 | private String display; 32 | private String loginHint; 33 | private String prompt; 34 | private String responseMode; 35 | 36 | private String logoutUrl; 37 | 38 | public String getAppId() { 39 | return appId; 40 | } 41 | 42 | public void setAppId(String appId) { 43 | this.appId = appId; 44 | } 45 | 46 | public String getAuthorizationBaseUrl() { 47 | return authorizationBaseUrl; 48 | } 49 | 50 | public void setAuthorizationBaseUrl(String authorizationBaseUrl) { 51 | this.authorizationBaseUrl = authorizationBaseUrl; 52 | } 53 | 54 | public String getAccessTokenEndpoint() { 55 | return accessTokenEndpoint; 56 | } 57 | 58 | public void setAccessTokenEndpoint(String accessTokenEndpoint) { 59 | this.accessTokenEndpoint = accessTokenEndpoint; 60 | } 61 | 62 | public String getResourceUrl() { 63 | return resourceUrl; 64 | } 65 | 66 | public void setResourceUrl(String resourceUrl) { 67 | this.resourceUrl = resourceUrl; 68 | } 69 | 70 | public boolean isLogsEnabled() { 71 | return logsEnabled; 72 | } 73 | 74 | public void setLogsEnabled(boolean logsEnabled) { 75 | this.logsEnabled = logsEnabled; 76 | } 77 | 78 | public String getResponseType() { 79 | return responseType; 80 | } 81 | 82 | public void setResponseType(String responseType) { 83 | this.responseType = responseType; 84 | } 85 | 86 | public String getScope() { 87 | return scope; 88 | } 89 | 90 | public void setScope(String scope) { 91 | this.scope = scope; 92 | } 93 | 94 | public String getState() { 95 | return state; 96 | } 97 | 98 | public void setState(String state) { 99 | this.state = state; 100 | } 101 | 102 | public String getRedirectUrl() { 103 | return redirectUrl; 104 | } 105 | 106 | public void setRedirectUrl(String redirectUrl) { 107 | this.redirectUrl = redirectUrl; 108 | } 109 | 110 | public String getCustomHandlerClass() { 111 | return customHandlerClass; 112 | } 113 | 114 | public void setCustomHandlerClass(String customHandlerClass) { 115 | this.customHandlerClass = customHandlerClass; 116 | } 117 | 118 | public boolean isPkceEnabled() { 119 | return pkceEnabled; 120 | } 121 | 122 | public void setPkceEnabled(boolean pkceEnabled) { 123 | this.pkceEnabled = pkceEnabled; 124 | } 125 | 126 | public String getPkceCodeVerifier() { 127 | return pkceCodeVerifier; 128 | } 129 | 130 | public void setPkceCodeVerifier(String pkceCodeVerifier) { 131 | this.pkceCodeVerifier = pkceCodeVerifier; 132 | } 133 | 134 | public Map getAdditionalParameters() { 135 | return additionalParameters; 136 | } 137 | 138 | public void setAdditionalParameters(Map additionalParameters) { 139 | this.additionalParameters = additionalParameters; 140 | } 141 | 142 | public void addAdditionalParameter(String key, String value) { 143 | if (key != null && value != null) { 144 | if (this.additionalParameters == null) { 145 | this.additionalParameters = new HashMap<>(); 146 | } 147 | this.additionalParameters.put(key, value); 148 | } 149 | } 150 | 151 | public String getDisplay() { 152 | return display; 153 | } 154 | 155 | public void setDisplay(String display) { 156 | this.display = display; 157 | } 158 | 159 | public String getLoginHint() { 160 | return loginHint; 161 | } 162 | 163 | public void setLoginHint(String loginHint) { 164 | this.loginHint = loginHint; 165 | } 166 | 167 | public String getPrompt() { 168 | return prompt; 169 | } 170 | 171 | public void setPrompt(String prompt) { 172 | this.prompt = prompt; 173 | } 174 | 175 | public String getResponseMode() { 176 | return responseMode; 177 | } 178 | 179 | public void setResponseMode(String responseMode) { 180 | this.responseMode = responseMode; 181 | } 182 | 183 | public boolean isHandleResultOnNewIntent() { 184 | return handleResultOnNewIntent; 185 | } 186 | 187 | public void setHandleResultOnNewIntent(boolean handleResultOnNewIntent) { 188 | this.handleResultOnNewIntent = handleResultOnNewIntent; 189 | } 190 | 191 | public boolean isHandleResultOnActivityResult() { 192 | return handleResultOnActivityResult; 193 | } 194 | 195 | public void setHandleResultOnActivityResult(boolean handleResultOnActivityResult) { 196 | this.handleResultOnActivityResult = handleResultOnActivityResult; 197 | } 198 | 199 | public Map getAdditionalResourceHeaders() { 200 | return additionalResourceHeaders; 201 | } 202 | 203 | public void setAdditionalResourceHeaders(Map additionalResourceHeaders) { 204 | this.additionalResourceHeaders = additionalResourceHeaders; 205 | } 206 | 207 | public void addAdditionalResourceHeader(String key, String value) { 208 | if (key != null && value != null) { 209 | if (this.additionalResourceHeaders == null) { 210 | this.additionalResourceHeaders = new HashMap<>(); 211 | } 212 | this.additionalResourceHeaders.put(key, value); 213 | } 214 | } 215 | 216 | public String getLogoutUrl() { 217 | return logoutUrl; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | public class OAuth2RefreshTokenOptions { 4 | 5 | private String appId; 6 | private String accessTokenEndpoint; 7 | private String refreshToken; 8 | private String scope; 9 | 10 | public String getAppId() { 11 | return appId; 12 | } 13 | 14 | public void setAppId(String appId) { 15 | this.appId = appId; 16 | } 17 | 18 | public String getAccessTokenEndpoint() { 19 | return accessTokenEndpoint; 20 | } 21 | 22 | public void setAccessTokenEndpoint(String accessTokenEndpoint) { 23 | this.accessTokenEndpoint = accessTokenEndpoint; 24 | } 25 | 26 | public String getRefreshToken() { 27 | return refreshToken; 28 | } 29 | 30 | public void setRefreshToken(String refreshToken) { 31 | this.refreshToken = refreshToken; 32 | } 33 | 34 | public String getScope() { 35 | return scope; 36 | } 37 | 38 | public void setScope(String scope) { 39 | this.scope = scope; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | import com.getcapacitor.JSObject; 4 | import net.openid.appauth.AuthorizationResponse; 5 | import net.openid.appauth.TokenResponse; 6 | 7 | public abstract class OAuth2Utils { 8 | 9 | public static void assignResponses( 10 | JSObject resp, 11 | String accessToken, 12 | AuthorizationResponse authorizationResponse, 13 | TokenResponse accessTokenResponse 14 | ) { 15 | // #154 16 | if (authorizationResponse != null) { 17 | resp.put("authorization_response", authorizationResponse.jsonSerialize()); 18 | } 19 | if (accessTokenResponse != null) { 20 | resp.put("access_token_response", accessTokenResponse.jsonSerialize()); 21 | } 22 | if (accessToken != null) { 23 | resp.put("access_token", accessToken); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceCallResult.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | import com.getcapacitor.JSObject; 4 | 5 | public class ResourceCallResult { 6 | 7 | private boolean error; 8 | private String errorMsg; 9 | private JSObject response; 10 | 11 | public boolean isError() { 12 | return error; 13 | } 14 | 15 | public void setError(boolean error) { 16 | this.error = error; 17 | } 18 | 19 | public JSObject getResponse() { 20 | return response; 21 | } 22 | 23 | public void setResponse(JSObject response) { 24 | this.response = response; 25 | } 26 | 27 | public String getErrorMsg() { 28 | return errorMsg; 29 | } 30 | 31 | public void setErrorMsg(String errorMsg) { 32 | this.errorMsg = errorMsg; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | import android.os.AsyncTask; 4 | import android.util.Log; 5 | import com.getcapacitor.JSObject; 6 | import com.getcapacitor.PluginCall; 7 | import java.io.BufferedReader; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.net.HttpURLConnection; 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.util.Map; 15 | import net.openid.appauth.AuthorizationResponse; 16 | import net.openid.appauth.TokenResponse; 17 | import org.json.JSONException; 18 | 19 | public class ResourceUrlAsyncTask extends AsyncTask { 20 | 21 | private static final String ERR_GENERAL = "ERR_GENERAL"; 22 | private static final String ERR_NO_ACCESS_TOKEN = "ERR_NO_ACCESS_TOKEN"; 23 | private static final String MSG_RETURNED_TO_JS = "Returned to JS:\n"; 24 | 25 | private final PluginCall pluginCall; 26 | private final OAuth2Options options; 27 | private final String logTag; 28 | private final AuthorizationResponse authorizationResponse; 29 | private final TokenResponse accessTokenResponse; 30 | 31 | public ResourceUrlAsyncTask( 32 | PluginCall pluginCall, 33 | OAuth2Options options, 34 | String logTag, 35 | AuthorizationResponse authorizationResponse, 36 | TokenResponse accessTokenResponse 37 | ) { 38 | this.pluginCall = pluginCall; 39 | this.options = options; 40 | this.logTag = logTag; 41 | this.authorizationResponse = authorizationResponse; 42 | this.accessTokenResponse = accessTokenResponse; 43 | } 44 | 45 | @Override 46 | protected ResourceCallResult doInBackground(String... tokens) { 47 | ResourceCallResult result = new ResourceCallResult(); 48 | 49 | String resourceUrl = options.getResourceUrl(); 50 | String accessToken = tokens[0]; 51 | if (resourceUrl != null) { 52 | Log.i(logTag, "Resource url: GET " + resourceUrl); 53 | if (accessToken != null) { 54 | Log.i(logTag, "Access token:\n" + accessToken); 55 | 56 | try { 57 | URL url = new URL(resourceUrl); 58 | HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 59 | conn.addRequestProperty("Authorization", String.format("Bearer %s", accessToken)); 60 | // additional headers 61 | if (options.getAdditionalResourceHeaders() != null) { 62 | for (Map.Entry entry : options.getAdditionalResourceHeaders().entrySet()) { 63 | conn.addRequestProperty(entry.getKey(), entry.getValue()); 64 | } 65 | } 66 | 67 | InputStream is = null; 68 | try { 69 | if ( 70 | conn.getResponseCode() >= HttpURLConnection.HTTP_OK && 71 | conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE 72 | ) { 73 | is = conn.getInputStream(); 74 | } else { 75 | is = conn.getErrorStream(); 76 | result.setError(true); 77 | } 78 | String resourceResponseBody = readInputStream(is); 79 | if (!result.isError()) { 80 | JSObject resultJson = new JSObject(resourceResponseBody); 81 | if (options.isLogsEnabled()) { 82 | Log.i(logTag, "Resource response:\n" + resourceResponseBody); 83 | } 84 | OAuth2Utils.assignResponses(resultJson, accessToken, this.authorizationResponse, this.accessTokenResponse); 85 | if (options.isLogsEnabled()) { 86 | Log.i(logTag, MSG_RETURNED_TO_JS + resultJson); 87 | } 88 | result.setResponse(resultJson); 89 | } else { 90 | result.setErrorMsg(resourceResponseBody); 91 | } 92 | } catch (IOException e) { 93 | Log.e(logTag, "", e); 94 | } catch (JSONException e) { 95 | Log.e(logTag, "Resource response no valid json.", e); 96 | } finally { 97 | conn.disconnect(); 98 | if (is != null) { 99 | is.close(); 100 | } 101 | } 102 | } catch (MalformedURLException e) { 103 | Log.e(logTag, "Invalid resource url '" + resourceUrl + "'", e); 104 | } catch (IOException e) { 105 | Log.e(logTag, "Unexpected error", e); 106 | } 107 | } else { 108 | if (options.isLogsEnabled()) { 109 | Log.i( 110 | logTag, 111 | "No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config." 112 | ); 113 | } 114 | pluginCall.reject(ERR_NO_ACCESS_TOKEN); 115 | } 116 | } else { 117 | JSObject json = new JSObject(); 118 | OAuth2Utils.assignResponses(json, accessToken, this.authorizationResponse, this.accessTokenResponse); 119 | if (options.isLogsEnabled()) { 120 | Log.i(logTag, MSG_RETURNED_TO_JS + json); 121 | } 122 | result.setResponse(json); 123 | } 124 | return result; 125 | } 126 | 127 | @Override 128 | protected void onPostExecute(ResourceCallResult response) { 129 | if (response != null) { 130 | if (!response.isError()) { 131 | pluginCall.resolve(response.getResponse()); 132 | } else { 133 | Log.e(logTag, response.getErrorMsg()); 134 | pluginCall.reject(ERR_GENERAL, response.getErrorMsg()); 135 | } 136 | } else { 137 | pluginCall.reject(ERR_GENERAL); 138 | } 139 | } 140 | 141 | private static String readInputStream(InputStream in) throws IOException { 142 | try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) { 143 | char[] buffer = new char[1024]; 144 | StringBuilder sb = new StringBuilder(); 145 | int readCount; 146 | while ((readCount = br.read(buffer)) != -1) { 147 | sb.append(buffer, 0, readCount); 148 | } 149 | return sb.toString(); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2.handler; 2 | 3 | public interface AccessTokenCallback { 4 | void onSuccess(String accessToken); 5 | 6 | void onCancel(); 7 | 8 | void onError(Exception error); 9 | } 10 | -------------------------------------------------------------------------------- /android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2.handler; 2 | 3 | import android.app.Activity; 4 | import com.getcapacitor.PluginCall; 5 | 6 | public interface OAuth2CustomHandler { 7 | void getAccessToken(Activity activity, PluginCall pluginCall, final AccessTokenCallback callback); 8 | 9 | boolean logout(Activity activity, PluginCall pluginCall); 10 | } 11 | -------------------------------------------------------------------------------- /android/src/main/res/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capacitor-community/generic-oauth2/2273cfb322967b78f0297100e2383f5628d459bd/android/src/main/res/.gitkeep -------------------------------------------------------------------------------- /android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | import android.util.Log; 4 | import com.getcapacitor.JSObject; 5 | import java.util.Map; 6 | import java.util.stream.Stream; 7 | import org.json.JSONException; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.params.ParameterizedTest; 12 | import org.junit.jupiter.params.provider.Arguments; 13 | import org.junit.jupiter.params.provider.MethodSource; 14 | 15 | public class ConfigUtilsTest { 16 | 17 | private static final String BASE_JSON = 18 | "{\n" + 19 | " \"doubleValue\": 123.4567,\n" + 20 | " \"floatValue\": 123.4,\n" + 21 | " \"intValue\": 1,\n" + 22 | " \"stringValue\": \"string\",\n" + 23 | " \"booleanValue\": true,\n" + 24 | " \"accessTokenEndpoint\": \"https://byteowls.com\",\n" + 25 | " \"first\": {\n" + 26 | " \"second\": {\n" + 27 | " \"third\": {\n" + 28 | " \"doubleValue\": 5.4,\n" + 29 | " \"floatValue\": 5.9,\n" + 30 | " \"intValue\": 2,\n" + 31 | " \"stringValue\": \"stringDeep\",\n" + 32 | " \"booleanValue\": false\n" + 33 | " }\n" + 34 | " }\n" + 35 | " },\n" + 36 | " \"map\": {\n" + 37 | " \"key1\": \"value1\",\n" + 38 | " \"key2\": \"value2\",\n" + 39 | " \"inMapNullable\": \"notEmpty\"\n" + 40 | " },\n" + 41 | " \"android\": {\n" + 42 | " \"stringValue\": \"stringAndroid\",\n" + 43 | " \"accessTokenEndpoint\": \"\",\n" + 44 | " \"map\": {\n" + 45 | " \"key1\": \"value1Android\",\n" + 46 | " \"key3\": \"value3Android\",\n" + 47 | " \"inMapNullable\": \"\"\n" + 48 | " }\n" + 49 | " },\n" + 50 | " \"empty\": \"\",\n" + 51 | " \"blank\": \" \"\n" + 52 | "}"; 53 | 54 | private JSObject jsObject; 55 | 56 | @BeforeEach 57 | public void setUp() { 58 | try { 59 | this.jsObject = new JSObject(BASE_JSON); 60 | } catch (Exception e) { 61 | Log.e("OAuth2", "", e); 62 | } 63 | } 64 | 65 | @Test 66 | public void getParamString() { 67 | String stringValue = ConfigUtils.getParamString(jsObject, "stringValue"); 68 | Assertions.assertNotNull(stringValue); 69 | Assertions.assertEquals("string", stringValue); 70 | } 71 | 72 | @Test 73 | public void getParam() { 74 | String stringValue = ConfigUtils.getParam(String.class, jsObject, "stringValue"); 75 | Assertions.assertNotNull(stringValue); 76 | Assertions.assertEquals("string", stringValue); 77 | 78 | Double doubleValue = ConfigUtils.getParam(Double.class, jsObject, "doubleValue"); 79 | Assertions.assertNotNull(doubleValue); 80 | } 81 | 82 | @Test 83 | public void getParamMap() { 84 | Map map = ConfigUtils.getParamMap(jsObject, "map"); 85 | Assertions.assertNotNull(map); 86 | Assertions.assertEquals("value1", map.get("key1")); 87 | } 88 | 89 | @Test 90 | public void getDeepestKey() { 91 | String deepestKey = ConfigUtils.getDeepestKey("com.example.deep"); 92 | Assertions.assertEquals("deep", deepestKey); 93 | 94 | deepestKey = ConfigUtils.getDeepestKey("com"); 95 | Assertions.assertEquals("com", deepestKey); 96 | } 97 | 98 | @Test 99 | public void getDeepestObject() { 100 | JSObject object = ConfigUtils.getDeepestObject(jsObject, "first.second.third"); 101 | Assertions.assertNotNull(object.getJSObject("third")); 102 | } 103 | 104 | @Test 105 | public void getOverwrittenAndroidParam() { 106 | String overwrittenString = ConfigUtils.getOverwrittenAndroidParam(String.class, jsObject, "stringValue"); 107 | Assertions.assertEquals("stringAndroid", overwrittenString); 108 | 109 | int intValue = ConfigUtils.getOverwrittenAndroidParam(Integer.class, jsObject, "intValue"); 110 | Assertions.assertEquals(1, intValue); 111 | } 112 | 113 | @Test 114 | public void getOverwrittenAndroidParamMap() { 115 | Map map = ConfigUtils.getOverwrittenAndroidParamMap(jsObject, "map"); 116 | Assertions.assertNotNull(map); 117 | Assertions.assertEquals("value1Android", map.get("key1")); 118 | Assertions.assertEquals("value2", map.get("key2")); 119 | Assertions.assertEquals("value3Android", map.get("key3")); 120 | } 121 | 122 | @Test 123 | public void overwriteWithEmpty() { 124 | String accessTokenEndpoint = "accessTokenEndpoint"; 125 | Assertions.assertNotNull(ConfigUtils.getParamString(jsObject, accessTokenEndpoint)); 126 | Assertions.assertEquals("", ConfigUtils.getOverwrittenAndroidParam(String.class, jsObject, accessTokenEndpoint)); 127 | 128 | String inMapNullable = "inMapNullable"; 129 | Map paramMap = ConfigUtils.getParamMap(jsObject, "map"); 130 | Assertions.assertNotNull(paramMap.get(inMapNullable)); 131 | Map androidParamMap = ConfigUtils.getOverwrittenAndroidParamMap(jsObject, "map"); 132 | Assertions.assertEquals("", androidParamMap.get(inMapNullable)); 133 | } 134 | 135 | @ParameterizedTest 136 | @MethodSource("getBooleanArguments") 137 | public void getOverwrittenBoolean(String json, String key, Boolean expected) throws JSONException { 138 | JSObject jsObject = new JSObject(json); 139 | Boolean actual = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, jsObject, key); 140 | if (expected == null) { 141 | Assertions.assertNull(actual); 142 | } else { 143 | Assertions.assertEquals(expected, actual); 144 | } 145 | } 146 | 147 | private static Stream getBooleanArguments() { 148 | return Stream.of( 149 | Arguments.of("{ \"pkceEnabled\": true, \"android\":{\"pkceEnabled\": false}}", "pkceEnabled", false), 150 | Arguments.of("{ \"pkceEnabled\": true}", "pkceEnabled", true), 151 | Arguments.of("{ \"pkceEnabled\": true}", "android.pkceEnabled", null), 152 | Arguments.of("{ \"pkceEnabled\": true, \"ios\":{\"pkceEnabled\": false}}", "pkceEnabled", true) 153 | ); 154 | } 155 | 156 | @Test 157 | public void getRandomString() { 158 | String randomString = ConfigUtils.getRandomString(8); 159 | Assertions.assertNotNull(randomString); 160 | Assertions.assertEquals(8, randomString.length()); 161 | } 162 | 163 | @Test 164 | public void empty() { 165 | // make sure the empty value stays empty 166 | String emptyValue = ConfigUtils.getParamString(jsObject, "empty"); 167 | Assertions.assertEquals(0, emptyValue.length()); 168 | } 169 | 170 | @Test 171 | public void blank() { 172 | // make sure the blank value stays blank 173 | String blankValue = ConfigUtils.getParamString(jsObject, "blank"); 174 | Assertions.assertEquals(" ", blankValue); 175 | } 176 | 177 | @Test 178 | public void trimToNull() { 179 | Assertions.assertNull(ConfigUtils.trimToNull(" ")); 180 | Assertions.assertNull(ConfigUtils.trimToNull(" ")); 181 | Assertions.assertNull(ConfigUtils.trimToNull("")); 182 | Assertions.assertEquals("a", ConfigUtils.trimToNull("a")); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.community.genericoauth2; 2 | 3 | import android.util.Log; 4 | import com.getcapacitor.JSObject; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class GenericOAuth2PluginTest { 10 | 11 | public static final String CLIENT_ID_ANDROID = "CLIENT_ID_ANDROID"; 12 | private GenericOAuth2Plugin plugin; 13 | 14 | @BeforeEach 15 | public void setup() { 16 | plugin = new GenericOAuth2Plugin(); 17 | } 18 | 19 | @Test 20 | public void allBooleanValues() { 21 | JSObject jsObject = loadJson( 22 | "{\n" + 23 | " \"appId\": \"CLIENT_ID\",\n" + 24 | " \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" + 25 | " \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" + 26 | " \"scope\": \"email profile\",\n" + 27 | " \"pkceEnabled\": true,\n" + 28 | " \"logsEnabled\": true,\n" + 29 | " \"resourceUrl\": \"https://www.googleapis.com/userinfo/v2/me\",\n" + 30 | " \"web\": {\n" + 31 | " \"redirectUrl\": \"http://localhost:4200\",\n" + 32 | " \"windowOptions\": \"height=600,left=0,top=0\"\n" + 33 | " },\n" + 34 | " \"android\": {\n" + 35 | " \"appId\": \"" + 36 | CLIENT_ID_ANDROID + 37 | "\",\n" + 38 | " \"redirectUrl\": \"com.company.project:/\",\n" + 39 | " \"handleResultMethod\": \"TEST\",\n" + 40 | " \"logsEnabled\": false,\n" + 41 | " \"handleResultOnNewIntent\": true,\n" + 42 | " \"handleResultOnActivityResult\": false,\n" + 43 | " \"responseType\": \"TOKEN\"\n" + 44 | " },\n" + 45 | " \"ios\": {\n" + 46 | " \"appId\": \"CLIENT_ID_IOS\",\n" + 47 | " \"responseType\": \"code\",\n" + 48 | " \"redirectUrl\": \"com.company.project:/\"\n" + 49 | " }\n" + 50 | "}\n" 51 | ); 52 | OAuth2Options options = plugin.buildAuthenticateOptions(jsObject); 53 | Assertions.assertNotNull(options); 54 | Assertions.assertTrue(options.isPkceEnabled()); 55 | Assertions.assertFalse(options.isLogsEnabled()); 56 | Assertions.assertTrue(options.isHandleResultOnNewIntent()); 57 | Assertions.assertFalse(options.isHandleResultOnActivityResult()); 58 | } 59 | 60 | @Test 61 | public void responseTypeToken() { 62 | JSObject jsObject = loadJson( 63 | "{\n" + 64 | " \"appId\": \"CLIENT_ID\",\n" + 65 | " \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" + 66 | " \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" + 67 | " \"scope\": \"email profile\",\n" + 68 | " \"pkceEnabled\": true,\n" + 69 | " \"resourceUrl\": \"https://www.googleapis.com/userinfo/v2/me\",\n" + 70 | " \"web\": {\n" + 71 | " \"redirectUrl\": \"http://localhost:4200\",\n" + 72 | " \"windowOptions\": \"height=600,left=0,top=0\"\n" + 73 | " },\n" + 74 | " \"android\": {\n" + 75 | " \"appId\": \"" + 76 | CLIENT_ID_ANDROID + 77 | "\",\n" + 78 | " \"redirectUrl\": \"com.company.project:/\",\n" + 79 | " \"handleResultMethod\": \"TEST\",\n" + 80 | " \"responseType\": \"TOKEN\"\n" + 81 | " },\n" + 82 | " \"ios\": {\n" + 83 | " \"appId\": \"CLIENT_ID_IOS\",\n" + 84 | " \"responseType\": \"code\",\n" + 85 | " \"redirectUrl\": \"com.company.project:/\"\n" + 86 | " }\n" + 87 | "}\n" 88 | ); 89 | OAuth2Options options = plugin.buildAuthenticateOptions(jsObject); 90 | Assertions.assertNotNull(options); 91 | Assertions.assertEquals(CLIENT_ID_ANDROID, options.getAppId()); 92 | Assertions.assertEquals("token", options.getResponseType().toLowerCase()); 93 | Assertions.assertTrue(options.isHandleResultOnActivityResult()); 94 | } 95 | 96 | @Test 97 | public void serverAuthorizationHandling() { 98 | JSObject jsObject = loadJson( 99 | "{\n" + 100 | " \"appId\": \"CLIENT_ID\",\n" + 101 | " \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" + 102 | " \"responseType\": \"code id_token\",\n" + 103 | " \"redirectUrl\": \"https://project.myserver.com/oauth\",\n" + 104 | " \"resourceUrl\": \"https://www.googleapis.com/userinfo/v2/me\",\n" + 105 | " \"scope\": \"email profile\",\n" + 106 | " \"web\": {\n" + 107 | " \"windowOptions\": \"height=600,left=0,top=0\"\n" + 108 | " },\n" + 109 | " \"android\": {\n" + 110 | " \"appId\": \"" + 111 | CLIENT_ID_ANDROID + 112 | "\"\n" + 113 | " },\n" + 114 | " \"ios\": {\n" + 115 | " \"appId\": \"CLIENT_ID_IOS\"\n" + 116 | " }\n" + 117 | "}\n" 118 | ); 119 | OAuth2Options options = plugin.buildAuthenticateOptions(jsObject); 120 | Assertions.assertNotNull(options.getAppId()); 121 | Assertions.assertEquals(CLIENT_ID_ANDROID, options.getAppId()); 122 | Assertions.assertNotNull(options.getAuthorizationBaseUrl()); 123 | Assertions.assertEquals("code id_token", options.getResponseType()); 124 | Assertions.assertNotNull(options.getRedirectUrl()); 125 | } 126 | 127 | @Test 128 | public void buildRefreshTokenOptions() { 129 | JSObject jsObject = loadJson( 130 | "{\n" + 131 | " \"appId\": \"CLIENT_ID\",\n" + 132 | " \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" + 133 | " \"refreshToken\": \"ss4f6sd5f4\",\n" + 134 | " \"scope\": \"email profile\"\n" + 135 | "}" 136 | ); 137 | OAuth2RefreshTokenOptions options = plugin.buildRefreshTokenOptions(jsObject); 138 | Assertions.assertNotNull(options); 139 | Assertions.assertNotNull(options.getAppId()); 140 | Assertions.assertNotNull(options.getAccessTokenEndpoint()); 141 | Assertions.assertNotNull(options.getRefreshToken()); 142 | Assertions.assertNotNull(options.getScope()); 143 | } 144 | 145 | private JSObject loadJson(String json) { 146 | try { 147 | return new JSObject(json); 148 | } catch (Exception e) { 149 | Log.e("OAuth2", "", e); 150 | } 151 | return null; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /ios/Plugin.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */; }; 11 | 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */; }; 12 | 451C6E972BE3BF4400D9577D /* OAuth2CustomHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451C6E962BE3BF4400D9577D /* OAuth2CustomHandler.swift */; }; 13 | 451C6E992BE3BF7200D9577D /* OAuth2SafariDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451C6E982BE3BF7200D9577D /* OAuth2SafariDelegate.swift */; }; 14 | 451C6E9B2BE3BF9F00D9577D /* GenericOAuth2Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451C6E9A2BE3BF9F00D9577D /* GenericOAuth2Plugin.swift */; }; 15 | 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFF88201F53D600D50D53 /* Plugin.framework */; }; 16 | 50ADFF97201F53D600D50D53 /* GenericOAuth2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFF96201F53D600D50D53 /* GenericOAuth2Tests.swift */; }; 17 | 50ADFF99201F53D600D50D53 /* GenericOAuth2Plugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 50ADFF8B201F53D600D50D53 /* GenericOAuth2Plugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; 18 | 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFFA52020D75100D50D53 /* Capacitor.framework */; }; 19 | 50ADFFA82020EE4F00D50D53 /* GenericOAuth2Plugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFFA72020EE4F00D50D53 /* GenericOAuth2Plugin.m */; }; 20 | 50E1A94820377CB70090CE1A /* GenericOAuth2Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1A94720377CB70090CE1A /* GenericOAuth2Plugin.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 50ADFF7F201F53D600D50D53 /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 50ADFF87201F53D600D50D53; 29 | remoteInfo = Plugin; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 451C6E962BE3BF4400D9577D /* OAuth2CustomHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CustomHandler.swift; sourceTree = ""; }; 36 | 451C6E982BE3BF7200D9577D /* OAuth2SafariDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2SafariDelegate.swift; sourceTree = ""; }; 37 | 451C6E9A2BE3BF9F00D9577D /* GenericOAuth2Plugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericOAuth2Plugin.swift; sourceTree = ""; }; 38 | 50ADFF88201F53D600D50D53 /* Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 50ADFF8B201F53D600D50D53 /* GenericOAuth2Plugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GenericOAuth2Plugin.h; sourceTree = ""; }; 40 | 50ADFF8C201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 50ADFF91201F53D600D50D53 /* PluginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PluginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 50ADFF96201F53D600D50D53 /* GenericOAuth2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericOAuth2Tests.swift; sourceTree = ""; }; 43 | 50ADFF98201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | 50ADFFA52020D75100D50D53 /* Capacitor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Capacitor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 50ADFFA72020EE4F00D50D53 /* GenericOAuth2Plugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GenericOAuth2Plugin.m; sourceTree = ""; }; 46 | 50E1A94720377CB70090CE1A /* GenericOAuth2Plugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericOAuth2Plugin.swift; sourceTree = ""; }; 47 | 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig"; sourceTree = ""; }; 48 | 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = ""; }; 49 | 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = ""; }; 50 | F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = ""; }; 51 | F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PluginTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 50ADFF84201F53D600D50D53 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */, 60 | 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | 50ADFF8E201F53D600D50D53 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */, 69 | 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */, 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | /* End PBXFrameworksBuildPhase section */ 74 | 75 | /* Begin PBXGroup section */ 76 | 50ADFF7E201F53D600D50D53 = { 77 | isa = PBXGroup; 78 | children = ( 79 | 50ADFF8A201F53D600D50D53 /* Plugin */, 80 | 50ADFF95201F53D600D50D53 /* PluginTests */, 81 | 50ADFF89201F53D600D50D53 /* Products */, 82 | 8C8E7744173064A9F6D438E3 /* Pods */, 83 | A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */, 84 | ); 85 | sourceTree = ""; 86 | }; 87 | 50ADFF89201F53D600D50D53 /* Products */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 50ADFF88201F53D600D50D53 /* Plugin.framework */, 91 | 50ADFF91201F53D600D50D53 /* PluginTests.xctest */, 92 | ); 93 | name = Products; 94 | sourceTree = ""; 95 | }; 96 | 50ADFF8A201F53D600D50D53 /* Plugin */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 451C6E982BE3BF7200D9577D /* OAuth2SafariDelegate.swift */, 100 | 451C6E962BE3BF4400D9577D /* OAuth2CustomHandler.swift */, 101 | 50E1A94720377CB70090CE1A /* GenericOAuth2Plugin.swift */, 102 | 50ADFF8B201F53D600D50D53 /* GenericOAuth2Plugin.h */, 103 | 451C6E9A2BE3BF9F00D9577D /* GenericOAuth2Plugin.swift */, 104 | 50ADFFA72020EE4F00D50D53 /* GenericOAuth2Plugin.m */, 105 | 50ADFF8C201F53D600D50D53 /* Info.plist */, 106 | ); 107 | path = Plugin; 108 | sourceTree = ""; 109 | }; 110 | 50ADFF95201F53D600D50D53 /* PluginTests */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 50ADFF96201F53D600D50D53 /* GenericOAuth2Tests.swift */, 114 | 50ADFF98201F53D600D50D53 /* Info.plist */, 115 | ); 116 | path = PluginTests; 117 | sourceTree = ""; 118 | }; 119 | 8C8E7744173064A9F6D438E3 /* Pods */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */, 123 | 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */, 124 | 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */, 125 | F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */, 126 | ); 127 | name = Pods; 128 | sourceTree = ""; 129 | }; 130 | A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 50ADFFA52020D75100D50D53 /* Capacitor.framework */, 134 | 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */, 135 | F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */, 136 | ); 137 | name = Frameworks; 138 | sourceTree = ""; 139 | }; 140 | /* End PBXGroup section */ 141 | 142 | /* Begin PBXHeadersBuildPhase section */ 143 | 50ADFF85201F53D600D50D53 /* Headers */ = { 144 | isa = PBXHeadersBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | 50ADFF99201F53D600D50D53 /* GenericOAuth2Plugin.h in Headers */, 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXHeadersBuildPhase section */ 152 | 153 | /* Begin PBXNativeTarget section */ 154 | 50ADFF87201F53D600D50D53 /* Plugin */ = { 155 | isa = PBXNativeTarget; 156 | buildConfigurationList = 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */; 157 | buildPhases = ( 158 | AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */, 159 | 50ADFF83201F53D600D50D53 /* Sources */, 160 | 50ADFF84201F53D600D50D53 /* Frameworks */, 161 | 50ADFF85201F53D600D50D53 /* Headers */, 162 | 50ADFF86201F53D600D50D53 /* Resources */, 163 | ); 164 | buildRules = ( 165 | ); 166 | dependencies = ( 167 | ); 168 | name = Plugin; 169 | productName = Plugin; 170 | productReference = 50ADFF88201F53D600D50D53 /* Plugin.framework */; 171 | productType = "com.apple.product-type.framework"; 172 | }; 173 | 50ADFF90201F53D600D50D53 /* PluginTests */ = { 174 | isa = PBXNativeTarget; 175 | buildConfigurationList = 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */; 176 | buildPhases = ( 177 | 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */, 178 | 50ADFF8D201F53D600D50D53 /* Sources */, 179 | 50ADFF8E201F53D600D50D53 /* Frameworks */, 180 | 50ADFF8F201F53D600D50D53 /* Resources */, 181 | 8E97F58B69A94C6503FC9C85 /* [CP] Embed Pods Frameworks */, 182 | ); 183 | buildRules = ( 184 | ); 185 | dependencies = ( 186 | 50ADFF94201F53D600D50D53 /* PBXTargetDependency */, 187 | ); 188 | name = PluginTests; 189 | productName = PluginTests; 190 | productReference = 50ADFF91201F53D600D50D53 /* PluginTests.xctest */; 191 | productType = "com.apple.product-type.bundle.unit-test"; 192 | }; 193 | /* End PBXNativeTarget section */ 194 | 195 | /* Begin PBXProject section */ 196 | 50ADFF7F201F53D600D50D53 /* Project object */ = { 197 | isa = PBXProject; 198 | attributes = { 199 | LastSwiftUpdateCheck = 0920; 200 | LastUpgradeCheck = 1160; 201 | ORGANIZATIONNAME = "Max Lynch"; 202 | TargetAttributes = { 203 | 50ADFF87201F53D600D50D53 = { 204 | CreatedOnToolsVersion = 9.2; 205 | LastSwiftMigration = 1100; 206 | ProvisioningStyle = Automatic; 207 | }; 208 | 50ADFF90201F53D600D50D53 = { 209 | CreatedOnToolsVersion = 9.2; 210 | LastSwiftMigration = 1100; 211 | ProvisioningStyle = Automatic; 212 | }; 213 | }; 214 | }; 215 | buildConfigurationList = 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */; 216 | compatibilityVersion = "Xcode 8.0"; 217 | developmentRegion = en; 218 | hasScannedForEncodings = 0; 219 | knownRegions = ( 220 | en, 221 | Base, 222 | ); 223 | mainGroup = 50ADFF7E201F53D600D50D53; 224 | productRefGroup = 50ADFF89201F53D600D50D53 /* Products */; 225 | projectDirPath = ""; 226 | projectRoot = ""; 227 | targets = ( 228 | 50ADFF87201F53D600D50D53 /* Plugin */, 229 | 50ADFF90201F53D600D50D53 /* PluginTests */, 230 | ); 231 | }; 232 | /* End PBXProject section */ 233 | 234 | /* Begin PBXResourcesBuildPhase section */ 235 | 50ADFF86201F53D600D50D53 /* Resources */ = { 236 | isa = PBXResourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | 50ADFF8F201F53D600D50D53 /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXResourcesBuildPhase section */ 250 | 251 | /* Begin PBXShellScriptBuildPhase section */ 252 | 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */ = { 253 | isa = PBXShellScriptBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | ); 257 | inputPaths = ( 258 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 259 | "${PODS_ROOT}/Manifest.lock", 260 | ); 261 | name = "[CP] Check Pods Manifest.lock"; 262 | outputPaths = ( 263 | "$(DERIVED_FILE_DIR)/Pods-PluginTests-checkManifestLockResult.txt", 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | shellPath = /bin/sh; 267 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 268 | showEnvVarsInLog = 0; 269 | }; 270 | 8E97F58B69A94C6503FC9C85 /* [CP] Embed Pods Frameworks */ = { 271 | isa = PBXShellScriptBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | ); 275 | inputPaths = ( 276 | "${PODS_ROOT}/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh", 277 | "${BUILT_PRODUCTS_DIR}/Capacitor/Capacitor.framework", 278 | "${BUILT_PRODUCTS_DIR}/CapacitorCordova/Cordova.framework", 279 | "${BUILT_PRODUCTS_DIR}/OAuthSwift/OAuthSwift.framework", 280 | ); 281 | name = "[CP] Embed Pods Frameworks"; 282 | outputPaths = ( 283 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Capacitor.framework", 284 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Cordova.framework", 285 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OAuthSwift.framework", 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | shellPath = /bin/sh; 289 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh\"\n"; 290 | showEnvVarsInLog = 0; 291 | }; 292 | AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */ = { 293 | isa = PBXShellScriptBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | ); 297 | inputPaths = ( 298 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 299 | "${PODS_ROOT}/Manifest.lock", 300 | ); 301 | name = "[CP] Check Pods Manifest.lock"; 302 | outputPaths = ( 303 | "$(DERIVED_FILE_DIR)/Pods-Plugin-checkManifestLockResult.txt", 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | shellPath = /bin/sh; 307 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 308 | showEnvVarsInLog = 0; 309 | }; 310 | /* End PBXShellScriptBuildPhase section */ 311 | 312 | /* Begin PBXSourcesBuildPhase section */ 313 | 50ADFF83201F53D600D50D53 /* Sources */ = { 314 | isa = PBXSourcesBuildPhase; 315 | buildActionMask = 2147483647; 316 | files = ( 317 | 451C6E972BE3BF4400D9577D /* OAuth2CustomHandler.swift in Sources */, 318 | 50E1A94820377CB70090CE1A /* GenericOAuth2Plugin.swift in Sources */, 319 | 50ADFFA82020EE4F00D50D53 /* GenericOAuth2Plugin.m in Sources */, 320 | 451C6E992BE3BF7200D9577D /* OAuth2SafariDelegate.swift in Sources */, 321 | 451C6E9B2BE3BF9F00D9577D /* GenericOAuth2Plugin.swift in Sources */, 322 | ); 323 | runOnlyForDeploymentPostprocessing = 0; 324 | }; 325 | 50ADFF8D201F53D600D50D53 /* Sources */ = { 326 | isa = PBXSourcesBuildPhase; 327 | buildActionMask = 2147483647; 328 | files = ( 329 | 50ADFF97201F53D600D50D53 /* GenericOAuth2Tests.swift in Sources */, 330 | ); 331 | runOnlyForDeploymentPostprocessing = 0; 332 | }; 333 | /* End PBXSourcesBuildPhase section */ 334 | 335 | /* Begin PBXTargetDependency section */ 336 | 50ADFF94201F53D600D50D53 /* PBXTargetDependency */ = { 337 | isa = PBXTargetDependency; 338 | target = 50ADFF87201F53D600D50D53 /* Plugin */; 339 | targetProxy = 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */; 340 | }; 341 | /* End PBXTargetDependency section */ 342 | 343 | /* Begin XCBuildConfiguration section */ 344 | 50ADFF9A201F53D600D50D53 /* Debug */ = { 345 | isa = XCBuildConfiguration; 346 | buildSettings = { 347 | ALWAYS_SEARCH_USER_PATHS = NO; 348 | CLANG_ANALYZER_NONNULL = YES; 349 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 350 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 351 | CLANG_CXX_LIBRARY = "libc++"; 352 | CLANG_ENABLE_MODULES = YES; 353 | CLANG_ENABLE_OBJC_ARC = YES; 354 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 355 | CLANG_WARN_BOOL_CONVERSION = YES; 356 | CLANG_WARN_COMMA = YES; 357 | CLANG_WARN_CONSTANT_CONVERSION = YES; 358 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 359 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 360 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 361 | CLANG_WARN_EMPTY_BODY = YES; 362 | CLANG_WARN_ENUM_CONVERSION = YES; 363 | CLANG_WARN_INFINITE_RECURSION = YES; 364 | CLANG_WARN_INT_CONVERSION = YES; 365 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 366 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 367 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 368 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 369 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 370 | CLANG_WARN_STRICT_PROTOTYPES = YES; 371 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 372 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 373 | CLANG_WARN_UNREACHABLE_CODE = YES; 374 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 375 | CODE_SIGN_IDENTITY = "iPhone Developer"; 376 | COPY_PHASE_STRIP = NO; 377 | CURRENT_PROJECT_VERSION = 1; 378 | DEBUG_INFORMATION_FORMAT = dwarf; 379 | ENABLE_STRICT_OBJC_MSGSEND = YES; 380 | ENABLE_TESTABILITY = YES; 381 | FRAMEWORK_SEARCH_PATHS = ( 382 | "\"${BUILT_PRODUCTS_DIR}/Capacitor\"", 383 | "\"${BUILT_PRODUCTS_DIR}/CapacitorCordova\"", 384 | ); 385 | GCC_C_LANGUAGE_STANDARD = gnu11; 386 | GCC_DYNAMIC_NO_PIC = NO; 387 | GCC_NO_COMMON_BLOCKS = YES; 388 | GCC_OPTIMIZATION_LEVEL = 0; 389 | GCC_PREPROCESSOR_DEFINITIONS = ( 390 | "DEBUG=1", 391 | "$(inherited)", 392 | ); 393 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 394 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 395 | GCC_WARN_UNDECLARED_SELECTOR = YES; 396 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 397 | GCC_WARN_UNUSED_FUNCTION = YES; 398 | GCC_WARN_UNUSED_VARIABLE = YES; 399 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 400 | MTL_ENABLE_DEBUG_INFO = YES; 401 | ONLY_ACTIVE_ARCH = YES; 402 | SDKROOT = iphoneos; 403 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 404 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 405 | VERSIONING_SYSTEM = "apple-generic"; 406 | VERSION_INFO_PREFIX = ""; 407 | }; 408 | name = Debug; 409 | }; 410 | 50ADFF9B201F53D600D50D53 /* Release */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | ALWAYS_SEARCH_USER_PATHS = NO; 414 | CLANG_ANALYZER_NONNULL = YES; 415 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 416 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 417 | CLANG_CXX_LIBRARY = "libc++"; 418 | CLANG_ENABLE_MODULES = YES; 419 | CLANG_ENABLE_OBJC_ARC = YES; 420 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 421 | CLANG_WARN_BOOL_CONVERSION = YES; 422 | CLANG_WARN_COMMA = YES; 423 | CLANG_WARN_CONSTANT_CONVERSION = YES; 424 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 425 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 426 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 427 | CLANG_WARN_EMPTY_BODY = YES; 428 | CLANG_WARN_ENUM_CONVERSION = YES; 429 | CLANG_WARN_INFINITE_RECURSION = YES; 430 | CLANG_WARN_INT_CONVERSION = YES; 431 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 432 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 433 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 434 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 435 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 436 | CLANG_WARN_STRICT_PROTOTYPES = YES; 437 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 438 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 439 | CLANG_WARN_UNREACHABLE_CODE = YES; 440 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 441 | CODE_SIGN_IDENTITY = "iPhone Developer"; 442 | COPY_PHASE_STRIP = NO; 443 | CURRENT_PROJECT_VERSION = 1; 444 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 445 | ENABLE_NS_ASSERTIONS = NO; 446 | ENABLE_STRICT_OBJC_MSGSEND = YES; 447 | FRAMEWORK_SEARCH_PATHS = ( 448 | "\"${BUILT_PRODUCTS_DIR}/Capacitor\"", 449 | "\"${BUILT_PRODUCTS_DIR}/CapacitorCordova\"", 450 | ); 451 | GCC_C_LANGUAGE_STANDARD = gnu11; 452 | GCC_NO_COMMON_BLOCKS = YES; 453 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 454 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 455 | GCC_WARN_UNDECLARED_SELECTOR = YES; 456 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 457 | GCC_WARN_UNUSED_FUNCTION = YES; 458 | GCC_WARN_UNUSED_VARIABLE = YES; 459 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 460 | MTL_ENABLE_DEBUG_INFO = NO; 461 | SDKROOT = iphoneos; 462 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 463 | VALIDATE_PRODUCT = YES; 464 | VERSIONING_SYSTEM = "apple-generic"; 465 | VERSION_INFO_PREFIX = ""; 466 | }; 467 | name = Release; 468 | }; 469 | 50ADFF9D201F53D600D50D53 /* Debug */ = { 470 | isa = XCBuildConfiguration; 471 | baseConfigurationReference = 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */; 472 | buildSettings = { 473 | CLANG_ENABLE_MODULES = YES; 474 | CODE_SIGN_IDENTITY = ""; 475 | CODE_SIGN_STYLE = Automatic; 476 | DEFINES_MODULE = YES; 477 | DYLIB_COMPATIBILITY_VERSION = 1; 478 | DYLIB_CURRENT_VERSION = 1; 479 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 480 | INFOPLIST_FILE = Plugin/Info.plist; 481 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 482 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 483 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)"; 484 | ONLY_ACTIVE_ARCH = YES; 485 | PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; 486 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 487 | SKIP_INSTALL = YES; 488 | SUPPORTS_MACCATALYST = NO; 489 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 490 | SWIFT_VERSION = 5.0; 491 | TARGETED_DEVICE_FAMILY = "1,2"; 492 | }; 493 | name = Debug; 494 | }; 495 | 50ADFF9E201F53D600D50D53 /* Release */ = { 496 | isa = XCBuildConfiguration; 497 | baseConfigurationReference = 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */; 498 | buildSettings = { 499 | CLANG_ENABLE_MODULES = YES; 500 | CODE_SIGN_IDENTITY = ""; 501 | CODE_SIGN_STYLE = Automatic; 502 | DEFINES_MODULE = YES; 503 | DYLIB_COMPATIBILITY_VERSION = 1; 504 | DYLIB_CURRENT_VERSION = 1; 505 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 506 | INFOPLIST_FILE = Plugin/Info.plist; 507 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 508 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 509 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; 510 | ONLY_ACTIVE_ARCH = NO; 511 | PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; 512 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 513 | SKIP_INSTALL = YES; 514 | SUPPORTS_MACCATALYST = NO; 515 | SWIFT_VERSION = 5.0; 516 | TARGETED_DEVICE_FAMILY = "1,2"; 517 | }; 518 | name = Release; 519 | }; 520 | 50ADFFA0201F53D600D50D53 /* Debug */ = { 521 | isa = XCBuildConfiguration; 522 | baseConfigurationReference = 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */; 523 | buildSettings = { 524 | CODE_SIGN_STYLE = Automatic; 525 | INFOPLIST_FILE = PluginTests/Info.plist; 526 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 527 | PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; 528 | PRODUCT_NAME = "$(TARGET_NAME)"; 529 | SWIFT_VERSION = 5.0; 530 | TARGETED_DEVICE_FAMILY = "1,2"; 531 | }; 532 | name = Debug; 533 | }; 534 | 50ADFFA1201F53D600D50D53 /* Release */ = { 535 | isa = XCBuildConfiguration; 536 | baseConfigurationReference = F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */; 537 | buildSettings = { 538 | CODE_SIGN_STYLE = Automatic; 539 | INFOPLIST_FILE = PluginTests/Info.plist; 540 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 541 | PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; 542 | PRODUCT_NAME = "$(TARGET_NAME)"; 543 | SWIFT_VERSION = 5.0; 544 | TARGETED_DEVICE_FAMILY = "1,2"; 545 | }; 546 | name = Release; 547 | }; 548 | /* End XCBuildConfiguration section */ 549 | 550 | /* Begin XCConfigurationList section */ 551 | 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */ = { 552 | isa = XCConfigurationList; 553 | buildConfigurations = ( 554 | 50ADFF9A201F53D600D50D53 /* Debug */, 555 | 50ADFF9B201F53D600D50D53 /* Release */, 556 | ); 557 | defaultConfigurationIsVisible = 0; 558 | defaultConfigurationName = Release; 559 | }; 560 | 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */ = { 561 | isa = XCConfigurationList; 562 | buildConfigurations = ( 563 | 50ADFF9D201F53D600D50D53 /* Debug */, 564 | 50ADFF9E201F53D600D50D53 /* Release */, 565 | ); 566 | defaultConfigurationIsVisible = 0; 567 | defaultConfigurationName = Release; 568 | }; 569 | 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */ = { 570 | isa = XCConfigurationList; 571 | buildConfigurations = ( 572 | 50ADFFA0201F53D600D50D53 /* Debug */, 573 | 50ADFFA1201F53D600D50D53 /* Release */, 574 | ); 575 | defaultConfigurationIsVisible = 0; 576 | defaultConfigurationName = Release; 577 | }; 578 | /* End XCConfigurationList section */ 579 | }; 580 | rootObject = 50ADFF7F201F53D600D50D53 /* Project object */; 581 | } 582 | -------------------------------------------------------------------------------- /ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /ios/Plugin.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Plugin/GenericOAuth2Plugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for Plugin. 4 | FOUNDATION_EXPORT double PluginVersionNumber; 5 | 6 | //! Project version string for Plugin. 7 | FOUNDATION_EXPORT const unsigned char PluginVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | -------------------------------------------------------------------------------- /ios/Plugin/GenericOAuth2Plugin.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | // Define the plugin using the CAP_PLUGIN Macro, and 5 | // each method the plugin supports using the CAP_PLUGIN_METHOD macro. 6 | CAP_PLUGIN(GenericOAuth2Plugin, "GenericOAuth2", 7 | CAP_PLUGIN_METHOD(refreshToken, CAPPluginReturnPromise); 8 | CAP_PLUGIN_METHOD(authenticate, CAPPluginReturnPromise); 9 | CAP_PLUGIN_METHOD(logout, CAPPluginReturnPromise); 10 | ) 11 | -------------------------------------------------------------------------------- /ios/Plugin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/Plugin/OAuth2CustomHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Capacitor 3 | 4 | @objc public protocol OAuth2CustomHandler: NSObjectProtocol { 5 | 6 | init() 7 | 8 | func getAccessToken(viewController: UIViewController, call: CAPPluginCall, 9 | success: @escaping (_ accessToken: String) -> Void, 10 | cancelled: @escaping () -> Void, 11 | failure: @escaping (_ error: Error) -> Void) 12 | 13 | func logout(viewController: UIViewController, call: CAPPluginCall) -> Bool 14 | } 15 | -------------------------------------------------------------------------------- /ios/Plugin/OAuth2SafariDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Capacitor 3 | import SafariServices 4 | 5 | class OAuth2SafariDelegate: NSObject, SFSafariViewControllerDelegate { 6 | 7 | var pluginCall: CAPPluginCall 8 | 9 | init(_ call: CAPPluginCall) { 10 | self.pluginCall = call 11 | } 12 | 13 | func safariViewControllerDidFinish(_ controller: SFSafariViewController) { 14 | self.pluginCall.reject(GenericOAuth2Plugin.SharedConstants.ERR_USER_CANCELLED) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ios/PluginTests/GenericOAuth2Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Plugin 3 | 4 | class GenericOAuth2Tests: XCTestCase { 5 | override func setUp() { 6 | super.setUp() 7 | // Put setup code here. This method is called before the invocation of each test method in the class. 8 | } 9 | 10 | override func tearDown() { 11 | // Put teardown code here. This method is called after the invocation of each test method in the class. 12 | super.tearDown() 13 | } 14 | 15 | func testEcho() { 16 | // This is an example of a functional test case for a plugin. 17 | // Use XCTAssert and related functions to verify your tests produce the correct results. 18 | 19 | let implementation = GenericOAuth2() 20 | let value = "Hello, World!" 21 | let result = implementation.echo(value) 22 | 23 | XCTAssertEqual(value, result) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ios/PluginTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '13.0' 2 | 3 | def capacitor_pods 4 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 5 | use_frameworks! 6 | pod 'Capacitor', :path => '../node_modules/@capacitor/ios' 7 | pod 'CapacitorCordova', :path => '../node_modules/@capacitor/ios' 8 | pod 'OAuthSwift', :git => 'https://github.com/OAuthSwift/OAuthSwift.git' 9 | end 10 | 11 | target 'Plugin' do 12 | capacitor_pods 13 | end 14 | 15 | target 'PluginTests' do 16 | capacitor_pods 17 | end 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | verbose: true, 4 | testEnvironment: 'node', 5 | globals: { 6 | window: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@capacitor-community/generic-oauth2", 3 | "version": "6.1.0", 4 | "description": "Capacitor OAuth 2 client plugin", 5 | "main": "dist/plugin.cjs.js", 6 | "module": "dist/esm/index.js", 7 | "types": "dist/esm/index.d.ts", 8 | "unpkg": "dist/plugin.js", 9 | "files": [ 10 | "android/src/main/", 11 | "android/build.gradle", 12 | "dist/", 13 | "ios/Plugin/", 14 | "CapacitorCommunityGenericOAuth2.podspec" 15 | ], 16 | "author": "", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/capacitor-community/generic-oauth2.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/capacitor-community/generic-oauth2/issues" 24 | }, 25 | "keywords": [ 26 | "capacitor", 27 | "capacitor-plugin", 28 | "oauth2", 29 | "oauth2-client", 30 | "social-login" 31 | ], 32 | "scripts": { 33 | "verify": "npm run verify:ios && npm run verify:android && npm run verify:web", 34 | "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin -destination generic/platform=iOS && cd ..", 35 | "verify:android": "cd android && ./gradlew clean build test && cd ..", 36 | "verify:web": "npm run build", 37 | "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", 38 | "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format", 39 | "eslint": "eslint . --ext ts", 40 | "prettier": "prettier \"**/*.{css,html,ts,js,java}\"", 41 | "swiftlint": "node-swiftlint", 42 | "docgen": "docgen --api GenericOAuth2Plugin --output-readme README.md --output-json dist/docs.json", 43 | "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs", 44 | "clean": "rimraf ./dist", 45 | "watch": "tsc --watch", 46 | "test": "jest", 47 | "removePacked": "rimraf -g capacitor-community-generic-oauth2-*.tgz", 48 | "publish:locally": "npm run removePacked && npm run build && npm pack", 49 | "prepublishOnly": "npm run build" 50 | }, 51 | "devDependencies": { 52 | "@capacitor/android": "6.1.1", 53 | "@capacitor/core": "6.1.1", 54 | "@capacitor/docgen": "0.2.2", 55 | "@capacitor/ios": "6.1.1", 56 | "@ionic/eslint-config": "0.4.0", 57 | "@ionic/prettier-config": "4.0.0", 58 | "@ionic/swiftlint-config": "1.1.2", 59 | "@types/jest": "29.5.12", 60 | "eslint": "8.57.0", 61 | "jest": "29.7.0", 62 | "prettier": "3.3.3", 63 | "prettier-plugin-java": "2.6.4", 64 | "rimraf": "6.0.1", 65 | "rollup": "4.19.1", 66 | "swiftlint": "1.0.2", 67 | "ts-jest": "29.2.3", 68 | "typescript": "5.5.4" 69 | }, 70 | "peerDependencies": { 71 | "@capacitor/core": ">=6" 72 | }, 73 | "prettier": "@ionic/prettier-config", 74 | "swiftlint": "@ionic/swiftlint-config", 75 | "eslintConfig": { 76 | "extends": "@ionic/eslint-config/recommended" 77 | }, 78 | "capacitor": { 79 | "ios": { 80 | "src": "ios" 81 | }, 82 | "android": { 83 | "src": "android" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'dist/esm/index.js', 3 | output: [ 4 | { 5 | file: 'dist/plugin.js', 6 | format: 'iife', 7 | name: 'capacitorGenericOAuth2', 8 | globals: { 9 | '@capacitor/core': 'capacitorExports', 10 | }, 11 | sourcemap: true, 12 | inlineDynamicImports: true, 13 | }, 14 | { 15 | file: 'dist/plugin.cjs.js', 16 | format: 'cjs', 17 | sourcemap: true, 18 | inlineDynamicImports: true, 19 | }, 20 | ], 21 | external: ['@capacitor/core'], 22 | }; 23 | -------------------------------------------------------------------------------- /src/definitions.ts: -------------------------------------------------------------------------------- 1 | export interface GenericOAuth2Plugin { 2 | /** 3 | * Authenticate against a OAuth 2 provider. 4 | * @param {OAuth2AuthenticateOptions} options 5 | * @returns {Promise} the resource url response 6 | */ 7 | authenticate(options: OAuth2AuthenticateOptions): Promise; 8 | /** 9 | * Listens for OAuth implicit redirect flow queryString CODE to generate an access_token 10 | * @param {OAuth2RedirectAuthenticationOptions} options 11 | * @returns {Promise} the token endpoint response 12 | */ 13 | redirectFlowCodeListener( 14 | options: ImplicitFlowRedirectOptions, 15 | ): Promise; 16 | /** 17 | * Get a new access token based on the given refresh token. 18 | * @param {OAuth2RefreshTokenOptions} options 19 | * @returns {Promise} the token endpoint response 20 | */ 21 | refreshToken(options: OAuth2RefreshTokenOptions): Promise; 22 | /** 23 | * Logout from the authenticated OAuth 2 provider 24 | * @param {OAuth2AuthenticateOptions} options Although not all options are needed. We simply reuse the options from authenticate 25 | * @param {String} id_token Optional idToken, only for Android 26 | * @returns {Promise} true if the logout was successful else false. 27 | */ 28 | logout( 29 | options: OAuth2AuthenticateOptions, 30 | id_token?: string, 31 | ): Promise; 32 | } 33 | 34 | export interface ImplicitFlowRedirectOptions extends OAuth2AuthenticateOptions { 35 | /** 36 | * The URL where we get the code 37 | */ 38 | response_url: string; 39 | } 40 | 41 | export interface OAuth2RefreshTokenOptions { 42 | /** 43 | * The app id (client id) you get from the oauth provider like Google, Facebook,... 44 | */ 45 | appId: string; 46 | /** 47 | * Url for retrieving the access_token. 48 | */ 49 | accessTokenEndpoint: string; 50 | /** 51 | * The refresh token that will be used to obtain the new access token. 52 | */ 53 | refreshToken: string; 54 | /** 55 | * A space-delimited list of permissions that identify the resources that your application could access on the user's behalf. 56 | */ 57 | scope?: string; 58 | } 59 | 60 | export interface OAuth2AuthenticateBaseOptions { 61 | /** 62 | * The app id (client id) you get from the oauth provider like Google, Facebook,... 63 | * 64 | * required! 65 | */ 66 | appId?: string; 67 | /** 68 | * The base url for retrieving tokens depending on the response type from a OAuth 2 provider. e.g. https://accounts.google.com/o/oauth2/auth 69 | * 70 | * required! 71 | */ 72 | authorizationBaseUrl?: string; 73 | /** 74 | * Tells the authorization server which grant to execute. Be aware that a full code flow is not supported as clientCredentials are not included in requests. 75 | * 76 | * But you can retrieve the authorizationCode if you don't set a accessTokenEndpoint. 77 | * 78 | * required! 79 | */ 80 | responseType?: string; 81 | /** 82 | * Url to which the oauth provider redirects after authentication. 83 | * 84 | * required! 85 | */ 86 | redirectUrl?: string; 87 | /** 88 | * Url for retrieving the access_token by the authorization code flow. 89 | */ 90 | accessTokenEndpoint?: string; 91 | /** 92 | * Protected resource url. For authentication you only need the basic user details. 93 | */ 94 | resourceUrl?: string; 95 | /** 96 | * Enable PKCE if you need it. 97 | */ 98 | pkceEnabled?: boolean; 99 | /** 100 | * A space-delimited list of permissions that identify the resources that your application could access on the user's behalf. 101 | * If you want to get a refresh token, you most likely will need the offline_access scope (only supported in Code Flow!) 102 | */ 103 | scope?: string; 104 | /** 105 | * A unique alpha numeric string used to prevent CSRF. If not set the plugin automatically generate a string 106 | * and sends it as using state is recommended. 107 | */ 108 | state?: string; 109 | /** 110 | * Additional parameters for the created authorization url 111 | */ 112 | additionalParameters?: { [key: string]: string }; 113 | /** 114 | * @since 3.0.0 115 | */ 116 | logsEnabled?: boolean; 117 | /** 118 | * @since 3.1.0 ... not implemented yet! 119 | */ 120 | logoutUrl?: string; 121 | 122 | /** 123 | * Additional headers for resource url request 124 | * @since 3.0.0 125 | */ 126 | additionalResourceHeaders?: { [key: string]: string }; 127 | } 128 | 129 | export interface OAuth2AuthenticateOptions 130 | extends OAuth2AuthenticateBaseOptions { 131 | /** 132 | * Custom options for the platform "web" 133 | */ 134 | web?: WebOption; 135 | /** 136 | * Custom options for the platform "android" 137 | */ 138 | android?: AndroidOptions; 139 | /** 140 | * Custom options for the platform "ios" 141 | */ 142 | ios?: IosOptions; 143 | } 144 | 145 | export interface WebOption extends OAuth2AuthenticateBaseOptions { 146 | /** 147 | * Options for the window the plugin open for authentication. e.g. width=500,height=600,left=0,top=0 148 | */ 149 | windowOptions?: string; 150 | /** 151 | * Options for the window target. Defaults to _blank 152 | */ 153 | windowTarget?: string; 154 | /** 155 | * Whether to send the cache control header with the token request, unsupported by some providers. Defaults to true. 156 | */ 157 | sendCacheControlHeader?: boolean; 158 | } 159 | 160 | export interface AndroidOptions extends OAuth2AuthenticateBaseOptions { 161 | /** 162 | * Some oauth provider especially Facebook forces us to use their SDK for apps. 163 | * 164 | * Provide a class name implementing the 'CapacitorCommunityGenericOAuth2.OAuth2CustomHandler' protocol. 165 | */ 166 | customHandlerClass?: string; 167 | /** 168 | * Alternative to handle the activity result. The `onNewIntent` method is only call if the App was killed while logging in. 169 | */ 170 | handleResultOnNewIntent?: boolean; 171 | /** 172 | * Default handling the activity result. 173 | */ 174 | handleResultOnActivityResult?: boolean; 175 | } 176 | 177 | export interface IosOptions extends OAuth2AuthenticateBaseOptions { 178 | /** 179 | * If true the iOS 13+ feature Sign in with Apple (SiWA) try to build the scope from the standard "scope" parameter. 180 | * 181 | * If false scope is set to email and fullName. 182 | */ 183 | siwaUseScope?: boolean; 184 | /** 185 | * Some oauth provider especially Facebook forces us to use their SDK for apps. 186 | * 187 | * Provide a class name implementing the 'CapacitorCommunityGenericOAuth2.OAuth2CustomHandler' protocol. 188 | */ 189 | customHandlerClass?: string; 190 | } 191 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { registerPlugin } from '@capacitor/core'; 2 | 3 | import type { GenericOAuth2Plugin } from './definitions'; 4 | 5 | const GenericOAuth2 = registerPlugin('GenericOAuth2', { 6 | web: () => import('./web').then(m => new m.GenericOAuth2Web()), 7 | }); 8 | 9 | export * from './definitions'; 10 | export { GenericOAuth2 }; 11 | -------------------------------------------------------------------------------- /src/web-utils.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import type { OAuth2AuthenticateOptions } from './definitions'; 3 | import { CryptoUtils, WebUtils } from './web-utils'; 4 | 5 | const mGetRandomValues = jest.fn().mockReturnValueOnce(new Uint32Array(10)); 6 | Object.defineProperty(window, 'crypto', { 7 | value: { getRandomValues: mGetRandomValues }, 8 | }); 9 | let store: { 10 | [k: string]: string; 11 | } = {}; 12 | const sessionStorageMock = { 13 | getItem: jest.fn().mockImplementation((key: string) => store[key] ?? null), 14 | setItem: jest 15 | .fn() 16 | .mockImplementation((key: string, value: string) => (store[key] = value)), 17 | removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), 18 | clear: jest.fn().mockImplementation(() => (store = {})), 19 | }; 20 | 21 | Object.defineProperty(window, 'sessionStorage', { 22 | value: sessionStorageMock, 23 | }); 24 | 25 | const googleOptions: OAuth2AuthenticateOptions = { 26 | appId: 'appId', 27 | authorizationBaseUrl: 'https://accounts.google.com/o/oauth2/auth', 28 | accessTokenEndpoint: 'https://www.googleapis.com/oauth2/v4/token', 29 | scope: 'email profile', 30 | resourceUrl: 'https://www.googleapis.com/userinfo/v2/me', 31 | pkceEnabled: false, 32 | web: { 33 | accessTokenEndpoint: '', 34 | redirectUrl: 'https://oauth2.byteowls.com/authorize', 35 | appId: 'webAppId', 36 | pkceEnabled: true, 37 | }, 38 | android: { 39 | responseType: 'code', 40 | }, 41 | ios: { 42 | responseType: 'code', 43 | }, 44 | }; 45 | 46 | const oneDriveOptions: OAuth2AuthenticateOptions = { 47 | appId: 'appId', 48 | authorizationBaseUrl: 49 | 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', 50 | accessTokenEndpoint: 51 | 'https://login.microsoftonline.com/common/oauth2/v2.0/token', 52 | scope: 'files.readwrite offline_access', 53 | responseType: 'code', 54 | additionalParameters: { 55 | willbeoverwritten: 'foobar', 56 | }, 57 | web: { 58 | redirectUrl: 'https://oauth2.byteowls.com/authorize', 59 | pkceEnabled: false, 60 | additionalParameters: { 61 | 'resource': 'resource_id', 62 | 'emptyParam': null!, 63 | ' ': 'test', 64 | 'nonce': WebUtils.randomString(10), 65 | }, 66 | }, 67 | android: { 68 | redirectUrl: 'com.byteowls.oauth2://authorize', 69 | }, 70 | ios: { 71 | redirectUrl: 'com.byteowls.oauth2://authorize', 72 | }, 73 | }; 74 | 75 | const implicitFlowOptions: OAuth2AuthenticateOptions = { 76 | ...oneDriveOptions, 77 | pkceEnabled: true, 78 | web: { 79 | ...oneDriveOptions.web, 80 | pkceEnabled: true, 81 | }, 82 | }; 83 | 84 | const redirectUrlOptions: OAuth2AuthenticateOptions = { 85 | appId: 'appId', 86 | authorizationBaseUrl: 87 | 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', 88 | responseType: 'code', 89 | redirectUrl: 'https://mycompany.server.com/oauth', 90 | scope: 'files.readwrite offline_access', 91 | additionalParameters: { 92 | willbeoverwritten: 'foobar', 93 | }, 94 | web: {}, 95 | android: { 96 | redirectUrl: 'com.byteowls.oauth2://authorize', 97 | }, 98 | ios: { 99 | redirectUrl: 'com.byteowls.oauth2://authorize', 100 | }, 101 | }; 102 | 103 | describe('base options processing', () => { 104 | it('should build a nested appId', () => { 105 | const appId = WebUtils.getAppId(googleOptions); 106 | expect(appId).toEqual('webAppId'); 107 | }); 108 | 109 | it('should build a overwritable string value', () => { 110 | const appId = WebUtils.getOverwritableValue(googleOptions, 'appId'); 111 | expect(appId).toEqual('webAppId'); 112 | }); 113 | 114 | it('should build a overwritable boolean value', () => { 115 | const pkceEnabled = WebUtils.getOverwritableValue( 116 | googleOptions, 117 | 'pkceEnabled', 118 | ); 119 | expect(pkceEnabled).toBeTruthy(); 120 | }); 121 | 122 | it('should build a overwritable additional parameters map', () => { 123 | const additionalParameters = WebUtils.getOverwritableValue<{ 124 | [key: string]: string; 125 | }>(oneDriveOptions, 'additionalParameters'); 126 | expect(additionalParameters).not.toBeUndefined(); 127 | expect(additionalParameters['resource']).toEqual('resource_id'); 128 | }); 129 | 130 | it('must not contain overwritten additional parameters', () => { 131 | const additionalParameters = WebUtils.getOverwritableValue<{ 132 | [key: string]: string; 133 | }>(oneDriveOptions, 'additionalParameters'); 134 | expect(additionalParameters['willbeoverwritten']).toBeUndefined(); 135 | }); 136 | 137 | it('must have a base redirect url', () => { 138 | const redirectUrl = WebUtils.getOverwritableValue( 139 | redirectUrlOptions, 140 | 'redirectUrl', 141 | ); 142 | expect(redirectUrl).toBeDefined(); 143 | }); 144 | 145 | it('must be overwritten by empty string from web section', () => { 146 | const accessTokenEndpoint = WebUtils.getOverwritableValue( 147 | googleOptions, 148 | 'accessTokenEndpoint', 149 | ); 150 | expect(accessTokenEndpoint).toStrictEqual(''); 151 | }); 152 | 153 | it('must not be overwritten if no key exists in web section', () => { 154 | const accessTokenEndpoint = WebUtils.getOverwritableValue( 155 | googleOptions, 156 | 'scope', 157 | ); 158 | expect(accessTokenEndpoint).toStrictEqual('email profile'); 159 | }); 160 | }); 161 | 162 | describe('web options', () => { 163 | it('should build web options', async () => { 164 | WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { 165 | expect(webOptions).not.toBeNull(); 166 | }); 167 | }); 168 | 169 | it('should not have a code verifier', async () => { 170 | WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { 171 | expect(webOptions.pkceCodeVerifier).toBeUndefined(); 172 | }); 173 | }); 174 | 175 | it('must not contain empty additional parameter', async () => { 176 | WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { 177 | expect(webOptions.additionalParameters[' ']).toBeUndefined(); 178 | expect(webOptions.additionalParameters['emptyParam']).toBeUndefined(); 179 | }); 180 | }); 181 | 182 | it('must have the sendCacheControlHeader enabled by default', async () => { 183 | WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { 184 | expect(webOptions.sendCacheControlHeader).toBeTruthy(); 185 | }); 186 | }); 187 | 188 | it('must allow the sendCacheControlHeader to be set to false', async () => { 189 | WebUtils.buildWebOptions({ 190 | web: { 191 | sendCacheControlHeader: false, 192 | }, 193 | }).then(webOptions => { 194 | expect(webOptions.sendCacheControlHeader).toBeFalsy(); 195 | }); 196 | }); 197 | 198 | describe('if pkceCode enabled', () => { 199 | beforeEach(() => { 200 | sessionStorageMock.clear(); 201 | }); 202 | describe('if a code exists in sessionStorage', () => { 203 | beforeEach(() => { 204 | const code = 'DEMO_CODE'; 205 | WebUtils.setCodeVerifier(code); 206 | }); 207 | it('should get the code correctly', async () => { 208 | const spy = jest.spyOn(WebUtils, 'getCodeVerifier'); 209 | const webOptions = await WebUtils.buildWebOptions(implicitFlowOptions); 210 | expect(spy).toBeCalled(); 211 | expect(webOptions.pkceCodeVerifier).toBe('DEMO_CODE'); 212 | }); 213 | }); 214 | describe("if a code doesn't exist in sessionStorage", () => { 215 | it('should set the code', async () => { 216 | const spy = jest.spyOn(WebUtils, 'setCodeVerifier'); 217 | const webOptions = await WebUtils.buildWebOptions(implicitFlowOptions); 218 | expect(webOptions.pkceCodeVerifier).toBeDefined(); 219 | expect(spy).toBeCalled(); 220 | }); 221 | }); 222 | }); 223 | }); 224 | 225 | describe('Url param extraction', () => { 226 | it('should return undefined on null url', () => { 227 | const paramObj = WebUtils.getUrlParams(null!); 228 | expect(paramObj).toBeUndefined(); 229 | }); 230 | 231 | it('should return undefined on empty url', () => { 232 | const paramObj = WebUtils.getUrlParams(''); 233 | expect(paramObj).toBeUndefined(); 234 | }); 235 | 236 | it('should return undefined on url with spaces', () => { 237 | const paramObj = WebUtils.getUrlParams(' '); 238 | expect(paramObj).toBeUndefined(); 239 | }); 240 | 241 | it('should return undefined if no params in url', () => { 242 | const paramObj = WebUtils.getUrlParams('https://app.example.com/'); 243 | expect(paramObj).toBeUndefined(); 244 | }); 245 | 246 | it('should return undefined if no params in url search', () => { 247 | const paramObj = WebUtils.getUrlParams('https://app.example.com?'); 248 | expect(paramObj).toBeUndefined(); 249 | }); 250 | 251 | it('should return undefined if no params in url hash', () => { 252 | const paramObj = WebUtils.getUrlParams('https://app.example.com#'); 253 | expect(paramObj).toBeUndefined(); 254 | }); 255 | 256 | it('should remove invalid combinations one param', () => { 257 | const paramObj = WebUtils.getUrlParams('https://app.example.com?=test'); 258 | expect(paramObj).toBeUndefined(); 259 | }); 260 | 261 | it('should remove invalid combinations multiple param', () => { 262 | const paramObj = WebUtils.getUrlParams( 263 | 'https://app.example.com?=test&key1=param1', 264 | ); 265 | expect(paramObj).toEqual({ key1: 'param1' }); 266 | }); 267 | 268 | it('should extract work with a single param', () => { 269 | const paramObj = WebUtils.getUrlParams( 270 | 'https://app.example.com?access_token=testtoken', 271 | ); 272 | expect(paramObj!['access_token']).toStrictEqual('testtoken'); 273 | }); 274 | 275 | it('should extract a uuid state param', () => { 276 | const state = WebUtils.randomString(); 277 | const paramObj = WebUtils.getUrlParams( 278 | `https://app.example.com?state=${state}&access_token=testtoken`, 279 | ); 280 | expect(paramObj!['state']).toStrictEqual(state); 281 | }); 282 | 283 | it('should use query flag and ignore hash flag', () => { 284 | const random = WebUtils.randomString(); 285 | const foo = WebUtils.randomString(); 286 | const paramObj = WebUtils.getUrlParams( 287 | `https://app.example.com?random=${random}&foo=${foo}#ignored`, 288 | ); 289 | expect(paramObj!['random']).toStrictEqual(random); 290 | expect(paramObj!['foo']).toStrictEqual(foo); 291 | }); 292 | 293 | it('should use query flag with another question mark in a param', () => { 294 | const random = WebUtils.randomString(); 295 | const foo = WebUtils.randomString(); 296 | const paramObj = WebUtils.getUrlParams( 297 | `https://app.example.com?random=${random}&foo=${foo}?questionmark`, 298 | ); 299 | expect(paramObj!['random']).toStrictEqual(random); 300 | expect(paramObj!['foo']).toStrictEqual(`${foo}?questionmark`); 301 | }); 302 | 303 | it('should use hash flag and ignore query flag', () => { 304 | const random = WebUtils.randomString(); 305 | const foo = WebUtils.randomString(); 306 | const paramObj = WebUtils.getUrlParams( 307 | `https://app.example.com#random=${random}&foo=${foo}?ignored`, 308 | ); 309 | expect(paramObj!['random']).toStrictEqual(random); 310 | expect(paramObj!['foo']).toStrictEqual(`${foo}?ignored`); 311 | }); 312 | 313 | it('should use hash flag with another hash in a param', () => { 314 | const random = WebUtils.randomString(); 315 | const foo = WebUtils.randomString(); 316 | const paramObj = WebUtils.getUrlParams( 317 | `https://app.example.com#random=${random}&foo=${foo}#hash`, 318 | ); 319 | expect(paramObj!['random']).toStrictEqual(random); 320 | expect(paramObj!['foo']).toStrictEqual(`${foo}#hash`); 321 | }); 322 | 323 | it('should extract hash params correctly', () => { 324 | const random = WebUtils.randomString(20); 325 | const url = `http://localhost:4200/#state=${random}&access_token=ya29.a0ARrdaM-sdfsfsdfsdfsdfs-YGFHwg_lM6dePPaT_TunbpsdfsdfsdfsEG6vTVLsLJDDW 326 | tv5m1Q8_g3hXraaoELYGsjl53&token_type=Bearer&expires_in=3599&scope=email%20profile%20openid%20 327 | https://www.googleapis.com/auth/userinfo.email%20https://www.googleapis.com/auth/userinfo.profile&authuser=0&prompt=none`; 328 | const paramObj = WebUtils.getUrlParams(url); 329 | expect(paramObj!['access_token']).toBeDefined(); 330 | expect(paramObj!['token_type']).toStrictEqual('Bearer'); 331 | expect(paramObj!['prompt']).toBeDefined(); 332 | expect(paramObj!['state']).toStrictEqual(random); 333 | }); 334 | 335 | it('should extract hash params if search param indicator present', () => { 336 | const token = 'sldfskdjflsdf12302'; 337 | const url = `http://localhost:3000/login?#access_token=${token}`; 338 | const paramObj = WebUtils.getUrlParams(url); 339 | expect(paramObj!['access_token']).toStrictEqual(token); 340 | }); 341 | }); 342 | 343 | describe('Random string gen', () => { 344 | it('should generate a 10 letter string', () => { 345 | const expected = 10; 346 | const random = WebUtils.randomString(expected); 347 | expect(random.length).toStrictEqual(expected); 348 | }); 349 | 350 | it('should generate a 43 letter string as this is the minimum for PKCE', () => { 351 | const expected = 43; 352 | const random = WebUtils.randomString(expected); 353 | expect(random.length).toStrictEqual(expected); 354 | }); 355 | }); 356 | 357 | describe('Authorization url building', () => { 358 | it('should contain a nonce param', async () => { 359 | WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { 360 | const authorizationUrl = WebUtils.getAuthorizationUrl(webOptions); 361 | expect(authorizationUrl).toContain('nonce'); 362 | }); 363 | }); 364 | }); 365 | 366 | describe('Crypto utils', () => { 367 | it('base 64 simple', () => { 368 | const arr: Uint8Array = CryptoUtils.toUint8Array('tester'); 369 | const expected = CryptoUtils.toBase64(arr); 370 | expect(expected).toEqual('dGVzdGVy'); 371 | }); 372 | 373 | it('base 64 special char', () => { 374 | const arr: Uint8Array = CryptoUtils.toUint8Array('testerposfieppw2874929'); 375 | const expected = CryptoUtils.toBase64(arr); 376 | expect(expected).toEqual('dGVzdGVycG9zZmllcHB3Mjg3NDkyOQ=='); 377 | }); 378 | 379 | it('base 64 with space', () => { 380 | const arr: Uint8Array = CryptoUtils.toUint8Array('base64 encoder'); 381 | const expected = CryptoUtils.toBase64(arr); 382 | expect(expected).toEqual('YmFzZTY0IGVuY29kZXI='); 383 | }); 384 | 385 | it('base64url safe all base64 special chars included', () => { 386 | const expected = CryptoUtils.toBase64Url('YmFz+TY0IG/uY29kZXI='); 387 | expect(expected).toEqual('YmFz-TY0IG_uY29kZXI'); 388 | }); 389 | }); 390 | 391 | describe('additional resource headers', () => { 392 | const headerKey = 'Access-Control-Allow-Origin'; 393 | 394 | const options: OAuth2AuthenticateOptions = { 395 | appId: 'appId', 396 | authorizationBaseUrl: 397 | 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', 398 | accessTokenEndpoint: 399 | 'https://login.microsoftonline.com/common/oauth2/v2.0/token', 400 | scope: 'files.readwrite offline_access', 401 | responseType: 'code', 402 | additionalResourceHeaders: { 403 | 'Access-Control-Allow-Origin': 'will-be-overwritten', 404 | }, 405 | web: { 406 | redirectUrl: 'https://oauth2.byteowls.com/authorize', 407 | pkceEnabled: false, 408 | additionalResourceHeaders: { 409 | 'Access-Control-Allow-Origin': '*', 410 | }, 411 | }, 412 | }; 413 | 414 | it('should be defined', async () => { 415 | const webOptions = await WebUtils.buildWebOptions(options); 416 | expect(webOptions.additionalResourceHeaders[headerKey]).toBeDefined(); 417 | }); 418 | 419 | it('should equal *', async () => { 420 | const webOptions = await WebUtils.buildWebOptions(options); 421 | expect(webOptions.additionalResourceHeaders[headerKey]).toEqual('*'); 422 | }); 423 | }); 424 | 425 | describe('implicit redirect authentication flow helpers', () => { 426 | beforeEach(() => { 427 | sessionStorageMock.clear(); 428 | }); 429 | 430 | it('should set code in session storage', () => { 431 | const code = 'DEMO_CODE'; 432 | const codeSet = WebUtils.setCodeVerifier(code); 433 | expect(window.sessionStorage.setItem).toBeCalledWith( 434 | `I_Capacitor_GenericOAuth2Plugin_PKCE`, 435 | code, 436 | ); 437 | expect(codeSet).toEqual(true); 438 | }); 439 | 440 | it('should get code if it exists in sessionStorage', () => { 441 | const code = 'DEMO_CODE'; 442 | WebUtils.setCodeVerifier(code); 443 | const readCode = WebUtils.getCodeVerifier(); 444 | expect(readCode).toBe(code); 445 | expect(window.sessionStorage.getItem).toBeCalledWith( 446 | `I_Capacitor_GenericOAuth2Plugin_PKCE`, 447 | ); 448 | }); 449 | 450 | it("should get null if code doesn't exist in sessionStorage", () => { 451 | const readCode = WebUtils.getCodeVerifier(); 452 | expect(readCode).toBeNull(); 453 | expect(window.sessionStorage.getItem).toBeCalledWith( 454 | `I_Capacitor_GenericOAuth2Plugin_PKCE`, 455 | ); 456 | }); 457 | 458 | it('should remove the code from sessionStorage', () => { 459 | WebUtils.clearCodeVerifier(); 460 | expect(window.sessionStorage.removeItem).toBeCalledWith( 461 | `I_Capacitor_GenericOAuth2Plugin_PKCE`, 462 | ); 463 | }); 464 | }); 465 | -------------------------------------------------------------------------------- /src/web-utils.ts: -------------------------------------------------------------------------------- 1 | import type { OAuth2AuthenticateOptions } from './definitions'; 2 | // import sha256 from "fast-sha256"; 3 | 4 | export class WebUtils { 5 | /** 6 | * Public only for testing 7 | */ 8 | static getAppId(options: OAuth2AuthenticateOptions): string { 9 | return this.getOverwritableValue(options, 'appId'); 10 | } 11 | 12 | static getOverwritableValue( 13 | options: OAuth2AuthenticateOptions | any, 14 | key: string, 15 | ): T { 16 | let base = options[key]; 17 | if (options.web && key in options.web) { 18 | base = options.web[key]; 19 | } 20 | return base; 21 | } 22 | 23 | /** 24 | * Public only for testing 25 | */ 26 | static getAuthorizationUrl(options: WebOptions): string { 27 | let url = options.authorizationBaseUrl + '?client_id=' + options.appId; 28 | url += '&response_type=' + options.responseType; 29 | 30 | if (options.redirectUrl) { 31 | url += '&redirect_uri=' + options.redirectUrl; 32 | } 33 | if (options.scope) { 34 | url += '&scope=' + options.scope; 35 | } 36 | url += '&state=' + options.state; 37 | 38 | if (options.additionalParameters) { 39 | for (const key in options.additionalParameters) { 40 | url += '&' + key + '=' + options.additionalParameters[key]; 41 | } 42 | } 43 | 44 | if (options.pkceCodeChallenge) { 45 | url += '&code_challenge=' + options.pkceCodeChallenge; 46 | url += '&code_challenge_method=' + options.pkceCodeChallengeMethod; 47 | } 48 | return encodeURI(url); 49 | } 50 | 51 | static getTokenEndpointData(options: WebOptions, code: string): string { 52 | let body = ''; 53 | body += 54 | encodeURIComponent('grant_type') + 55 | '=' + 56 | encodeURIComponent('authorization_code') + 57 | '&'; 58 | body += 59 | encodeURIComponent('client_id') + 60 | '=' + 61 | encodeURIComponent(options.appId) + 62 | '&'; 63 | body += 64 | encodeURIComponent('redirect_uri') + 65 | '=' + 66 | encodeURIComponent(options.redirectUrl) + 67 | '&'; 68 | body += encodeURIComponent('code') + '=' + encodeURIComponent(code) + '&'; 69 | body += 70 | encodeURIComponent('code_verifier') + 71 | '=' + 72 | encodeURIComponent(options.pkceCodeVerifier); 73 | return body; 74 | } 75 | 76 | static setCodeVerifier(code: string): boolean { 77 | try { 78 | window.sessionStorage.setItem(`I_Capacitor_GenericOAuth2Plugin_PKCE`, code); 79 | return true; 80 | } catch (err) { 81 | return false; 82 | } 83 | } 84 | 85 | static clearCodeVerifier(): void { 86 | window.sessionStorage.removeItem(`I_Capacitor_GenericOAuth2Plugin_PKCE`); 87 | } 88 | 89 | static getCodeVerifier(): string | null { 90 | return window.sessionStorage.getItem(`I_Capacitor_GenericOAuth2Plugin_PKCE`); 91 | } 92 | 93 | /** 94 | * Public only for testing 95 | */ 96 | static getUrlParams(url: string): { [x: string]: string } | undefined { 97 | const urlString = `${url ?? ''}`.trim(); 98 | 99 | if (urlString.length === 0) { 100 | return; 101 | } 102 | 103 | const parsedUrl = new URL(urlString); 104 | if (!parsedUrl.search && !parsedUrl.hash) { 105 | return; 106 | } 107 | 108 | let urlParamStr; 109 | if (parsedUrl.search) { 110 | urlParamStr = parsedUrl.search.substr(1); 111 | } else { 112 | urlParamStr = parsedUrl.hash.substr(1); 113 | } 114 | 115 | const keyValuePairs: string[] = urlParamStr.split(`&`); 116 | return keyValuePairs.reduce<{ [x: string]: string } | undefined>( 117 | (accumulator, currentValue) => { 118 | const [key, val] = currentValue.split(`=`); 119 | if (key && key.length > 0) { 120 | return { 121 | ...accumulator, 122 | [key]: decodeURIComponent(val), 123 | }; 124 | } 125 | }, 126 | {}, 127 | ); 128 | } 129 | 130 | static randomString(length = 10): string { 131 | const haystack = 132 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; 133 | let randomStr; 134 | if (window.crypto) { 135 | let numberArray: Uint32Array = new Uint32Array(length); 136 | window.crypto.getRandomValues(numberArray); 137 | numberArray = numberArray.map(x => 138 | haystack.charCodeAt(x % haystack.length), 139 | ); 140 | 141 | const stringArray: string[] = []; 142 | numberArray.forEach(x => { 143 | stringArray.push(haystack.charAt(x % haystack.length)); 144 | }); 145 | randomStr = stringArray.join(''); 146 | } else { 147 | randomStr = ''; 148 | for (let i = 0; i < length; i++) { 149 | randomStr += haystack.charAt( 150 | Math.floor(Math.random() * haystack.length), 151 | ); 152 | } 153 | } 154 | return randomStr; 155 | } 156 | 157 | static async buildWebOptions( 158 | configOptions: OAuth2AuthenticateOptions, 159 | ): Promise { 160 | const webOptions = new WebOptions(); 161 | webOptions.appId = this.getAppId(configOptions); 162 | webOptions.authorizationBaseUrl = this.getOverwritableValue( 163 | configOptions, 164 | 'authorizationBaseUrl', 165 | ); 166 | webOptions.responseType = this.getOverwritableValue( 167 | configOptions, 168 | 'responseType', 169 | ); 170 | if (!webOptions.responseType) { 171 | webOptions.responseType = 'token'; 172 | } 173 | webOptions.redirectUrl = this.getOverwritableValue( 174 | configOptions, 175 | 'redirectUrl', 176 | ); 177 | // controlling parameters 178 | webOptions.resourceUrl = this.getOverwritableValue( 179 | configOptions, 180 | 'resourceUrl', 181 | ); 182 | webOptions.accessTokenEndpoint = this.getOverwritableValue( 183 | configOptions, 184 | 'accessTokenEndpoint', 185 | ); 186 | 187 | webOptions.pkceEnabled = this.getOverwritableValue( 188 | configOptions, 189 | 'pkceEnabled', 190 | ); 191 | webOptions.sendCacheControlHeader = 192 | this.getOverwritableValue(configOptions, 'sendCacheControlHeader') ?? 193 | webOptions.sendCacheControlHeader; 194 | if (webOptions.pkceEnabled) { 195 | const pkceCode = this.getCodeVerifier(); 196 | if (pkceCode) { 197 | webOptions.pkceCodeVerifier = pkceCode; 198 | } else { 199 | webOptions.pkceCodeVerifier = this.randomString(64); 200 | this.setCodeVerifier(webOptions.pkceCodeVerifier); 201 | } 202 | if (CryptoUtils.HAS_SUBTLE_CRYPTO) { 203 | await CryptoUtils.deriveChallenge(webOptions.pkceCodeVerifier).then( 204 | c => { 205 | webOptions.pkceCodeChallenge = c; 206 | webOptions.pkceCodeChallengeMethod = 'S256'; 207 | }, 208 | ); 209 | } else { 210 | webOptions.pkceCodeChallenge = webOptions.pkceCodeVerifier; 211 | webOptions.pkceCodeChallengeMethod = 'plain'; 212 | } 213 | } 214 | webOptions.scope = this.getOverwritableValue(configOptions, 'scope'); 215 | webOptions.state = this.getOverwritableValue(configOptions, 'state'); 216 | if (!webOptions.state || webOptions.state.length === 0) { 217 | webOptions.state = this.randomString(20); 218 | } 219 | const parametersMapHelper = this.getOverwritableValue<{ 220 | [key: string]: string; 221 | }>(configOptions, 'additionalParameters'); 222 | if (parametersMapHelper) { 223 | webOptions.additionalParameters = {}; 224 | for (const key in parametersMapHelper) { 225 | if (key && key.trim().length > 0) { 226 | const value = parametersMapHelper[key]; 227 | if (value && value.trim().length > 0) { 228 | webOptions.additionalParameters[key] = value; 229 | } 230 | } 231 | } 232 | } 233 | const headersMapHelper = this.getOverwritableValue<{ 234 | [key: string]: string; 235 | }>(configOptions, 'additionalResourceHeaders'); 236 | if (headersMapHelper) { 237 | webOptions.additionalResourceHeaders = {}; 238 | for (const key in headersMapHelper) { 239 | if (key && key.trim().length > 0) { 240 | const value = headersMapHelper[key]; 241 | if (value && value.trim().length > 0) { 242 | webOptions.additionalResourceHeaders[key] = value; 243 | } 244 | } 245 | } 246 | } 247 | webOptions.logsEnabled = this.getOverwritableValue( 248 | configOptions, 249 | 'logsEnabled', 250 | ); 251 | 252 | return webOptions; 253 | } 254 | 255 | static buildWindowOptions( 256 | configOptions: OAuth2AuthenticateOptions, 257 | ): WebOptions { 258 | const windowOptions = new WebOptions(); 259 | if (configOptions.web) { 260 | if (configOptions.web.windowOptions) { 261 | windowOptions.windowOptions = configOptions.web.windowOptions; 262 | } 263 | if (configOptions.web.windowTarget) { 264 | windowOptions.windowTarget = configOptions.web.windowTarget; 265 | } 266 | } 267 | return windowOptions; 268 | } 269 | } 270 | 271 | export class CryptoUtils { 272 | static BASE64_CHARS = 273 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 274 | static HAS_SUBTLE_CRYPTO: boolean = 275 | typeof window !== 'undefined' && 276 | !!(window.crypto as any) && 277 | !!(window.crypto.subtle as any); 278 | 279 | static toUint8Array(str: string): Uint8Array { 280 | const buf = new ArrayBuffer(str.length); 281 | const bufView = new Uint8Array(buf); 282 | 283 | for (let i = 0; i < str.length; i++) { 284 | bufView[i] = str.charCodeAt(i); 285 | } 286 | return bufView; 287 | } 288 | 289 | static toBase64Url(base64: string): string { 290 | return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 291 | } 292 | 293 | static toBase64(bytes: Uint8Array): string { 294 | const len = bytes.length; 295 | let base64 = ''; 296 | for (let i = 0; i < len; i += 3) { 297 | base64 += this.BASE64_CHARS[bytes[i] >> 2]; 298 | base64 += this.BASE64_CHARS[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; 299 | base64 += 300 | this.BASE64_CHARS[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; 301 | base64 += this.BASE64_CHARS[bytes[i + 2] & 63]; 302 | } 303 | 304 | if (len % 3 === 2) { 305 | base64 = base64.substring(0, base64.length - 1) + '='; 306 | } else if (len % 3 === 1) { 307 | base64 = base64.substring(0, base64.length - 2) + '=='; 308 | } 309 | return base64; 310 | } 311 | 312 | static deriveChallenge(codeVerifier: string): Promise { 313 | if (codeVerifier.length < 43 || codeVerifier.length > 128) { 314 | return Promise.reject(new Error('ERR_PKCE_CODE_VERIFIER_INVALID_LENGTH')); 315 | } 316 | if (!CryptoUtils.HAS_SUBTLE_CRYPTO) { 317 | return Promise.reject(new Error('ERR_PKCE_CRYPTO_NOTSUPPORTED')); 318 | } 319 | 320 | return new Promise((resolve, reject) => { 321 | crypto.subtle.digest('SHA-256', this.toUint8Array(codeVerifier)).then( 322 | arrayBuffer => { 323 | return resolve( 324 | this.toBase64Url(this.toBase64(new Uint8Array(arrayBuffer))), 325 | ); 326 | }, 327 | error => reject(error), 328 | ); 329 | }); 330 | } 331 | } 332 | 333 | export class WebOptions { 334 | appId: string; 335 | authorizationBaseUrl: string; 336 | accessTokenEndpoint: string; 337 | resourceUrl: string; 338 | responseType: string; 339 | scope: string; 340 | sendCacheControlHeader = true; 341 | state: string; 342 | redirectUrl: string; 343 | logsEnabled: boolean; 344 | windowOptions: string; 345 | windowTarget = '_blank'; 346 | 347 | pkceEnabled: boolean; 348 | pkceCodeVerifier: string; 349 | pkceCodeChallenge: string; 350 | pkceCodeChallengeMethod: string; 351 | 352 | additionalParameters: { [key: string]: string }; 353 | additionalResourceHeaders: { [key: string]: string }; 354 | } 355 | -------------------------------------------------------------------------------- /src/web.ts: -------------------------------------------------------------------------------- 1 | import { WebPlugin } from '@capacitor/core'; 2 | 3 | import type { 4 | OAuth2AuthenticateOptions, 5 | GenericOAuth2Plugin, 6 | OAuth2RefreshTokenOptions, 7 | ImplicitFlowRedirectOptions, 8 | } from './definitions'; 9 | import type { WebOptions } from './web-utils'; 10 | import { WebUtils } from './web-utils'; 11 | 12 | export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { 13 | private webOptions: WebOptions; 14 | private windowHandle: Window | null; 15 | private intervalId: number; 16 | private loopCount = 2000; 17 | private intervalLength = 100; 18 | private windowClosedByPlugin: boolean; 19 | 20 | /** 21 | * Get a new access token using an existing refresh token. 22 | */ 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | async refreshToken(_options: OAuth2RefreshTokenOptions): Promise { 25 | return new Promise((_resolve, reject) => { 26 | reject(new Error('Functionality not implemented for PWAs yet')); 27 | }); 28 | } 29 | 30 | async redirectFlowCodeListener( 31 | options: ImplicitFlowRedirectOptions, 32 | ): Promise { 33 | this.webOptions = await WebUtils.buildWebOptions(options); 34 | return new Promise((resolve, reject) => { 35 | const urlParamObj = WebUtils.getUrlParams(options.response_url); 36 | if (urlParamObj) { 37 | const code = urlParamObj.code; 38 | if (code) { 39 | this.getAccessToken(urlParamObj, resolve, reject, code); 40 | } else { 41 | reject(new Error('Oauth Code parameter was not present in url.')); 42 | } 43 | } else { 44 | reject(new Error('Oauth Parameters where not present in url.')); 45 | } 46 | }); 47 | } 48 | 49 | async authenticate(options: OAuth2AuthenticateOptions): Promise { 50 | const windowOptions = WebUtils.buildWindowOptions(options); 51 | 52 | // we open the window first to avoid popups being blocked because of 53 | // the asynchronous buildWebOptions call 54 | this.windowHandle = window.open( 55 | '', 56 | windowOptions.windowTarget, 57 | windowOptions.windowOptions, 58 | ); 59 | 60 | this.webOptions = await WebUtils.buildWebOptions(options); 61 | return new Promise((resolve, reject) => { 62 | // validate 63 | if (!this.webOptions.appId || this.webOptions.appId.length == 0) { 64 | reject(new Error('ERR_PARAM_NO_APP_ID')); 65 | } else if ( 66 | !this.webOptions.authorizationBaseUrl || 67 | this.webOptions.authorizationBaseUrl.length == 0 68 | ) { 69 | reject(new Error('ERR_PARAM_NO_AUTHORIZATION_BASE_URL')); 70 | } else if ( 71 | !this.webOptions.redirectUrl || 72 | this.webOptions.redirectUrl.length == 0 73 | ) { 74 | reject(new Error('ERR_PARAM_NO_REDIRECT_URL')); 75 | } else if ( 76 | !this.webOptions.responseType || 77 | this.webOptions.responseType.length == 0 78 | ) { 79 | reject(new Error('ERR_PARAM_NO_RESPONSE_TYPE')); 80 | } else { 81 | // init internal control params 82 | let loopCount = this.loopCount; 83 | this.windowClosedByPlugin = false; 84 | // open window 85 | const authorizationUrl = WebUtils.getAuthorizationUrl(this.webOptions); 86 | if (this.webOptions.logsEnabled) { 87 | this.doLog('Authorization url: ' + authorizationUrl); 88 | } 89 | if (this.windowHandle) { 90 | this.windowHandle.location.href = authorizationUrl; 91 | } 92 | // wait for redirect and resolve the 93 | this.intervalId = window.setInterval(() => { 94 | if (loopCount-- < 0) { 95 | this.closeWindow(); 96 | } else if (this.windowHandle?.closed && !this.windowClosedByPlugin) { 97 | window.clearInterval(this.intervalId); 98 | reject(new Error('USER_CANCELLED')); 99 | } else { 100 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 101 | let href: string = undefined!; 102 | try { 103 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 104 | href = this.windowHandle!.location.href!; 105 | } catch (ignore) { 106 | // ignore DOMException: Blocked a frame with origin "http://localhost:4200" from accessing a cross-origin frame. 107 | } 108 | 109 | if ( 110 | href != null && 111 | href.indexOf(this.webOptions.redirectUrl) >= 0 112 | ) { 113 | if (this.webOptions.logsEnabled) { 114 | this.doLog('Url from Provider: ' + href); 115 | } 116 | const authorizationRedirectUrlParamObj = 117 | WebUtils.getUrlParams(href); 118 | if (authorizationRedirectUrlParamObj) { 119 | if (this.webOptions.logsEnabled) { 120 | this.doLog( 121 | 'Authorization response:', 122 | authorizationRedirectUrlParamObj, 123 | ); 124 | } 125 | window.clearInterval(this.intervalId); 126 | // check state 127 | if ( 128 | authorizationRedirectUrlParamObj.state === 129 | this.webOptions.state 130 | ) { 131 | if (this.webOptions.accessTokenEndpoint) { 132 | const authorizationCode = 133 | authorizationRedirectUrlParamObj.code; 134 | if (authorizationCode) { 135 | this.getAccessToken( 136 | authorizationRedirectUrlParamObj, 137 | resolve, 138 | reject, 139 | authorizationCode, 140 | ); 141 | } else { 142 | reject(new Error('ERR_NO_AUTHORIZATION_CODE')); 143 | } 144 | this.closeWindow(); 145 | } else { 146 | // if no accessTokenEndpoint exists request the resource 147 | this.requestResource( 148 | authorizationRedirectUrlParamObj.access_token, 149 | resolve, 150 | reject, 151 | authorizationRedirectUrlParamObj, 152 | ); 153 | } 154 | } else { 155 | if (this.webOptions.logsEnabled) { 156 | this.doLog( 157 | 'State from web options: ' + this.webOptions.state, 158 | ); 159 | this.doLog( 160 | 'State returned from provider: ' + 161 | authorizationRedirectUrlParamObj.state, 162 | ); 163 | } 164 | reject(new Error('ERR_STATES_NOT_MATCH')); 165 | this.closeWindow(); 166 | } 167 | } 168 | // this is no error no else clause required 169 | } 170 | } 171 | }, this.intervalLength); 172 | } 173 | }); 174 | } 175 | 176 | private readonly MSG_RETURNED_TO_JS = 'Returned to JS:'; 177 | 178 | private getAccessToken( 179 | authorizationRedirectUrlParamObj: { [p: string]: string } | undefined, 180 | resolve: (value: any) => void, 181 | reject: (reason?: any) => void, 182 | authorizationCode: string, 183 | ) { 184 | const tokenRequest = new XMLHttpRequest(); 185 | tokenRequest.onload = () => { 186 | WebUtils.clearCodeVerifier(); 187 | if (tokenRequest.status === 200) { 188 | const accessTokenResponse = JSON.parse(tokenRequest.response); 189 | if (this.webOptions.logsEnabled) { 190 | this.doLog('Access token response:', accessTokenResponse); 191 | } 192 | this.requestResource( 193 | accessTokenResponse.access_token, 194 | resolve, 195 | reject, 196 | authorizationRedirectUrlParamObj, 197 | accessTokenResponse, 198 | ); 199 | } 200 | }; 201 | tokenRequest.onerror = () => { 202 | this.doLog( 203 | 'ERR_GENERAL: See client logs. It might be CORS. Status text: ' + 204 | tokenRequest.statusText, 205 | ); 206 | reject(new Error('ERR_GENERAL')); 207 | }; 208 | tokenRequest.open('POST', this.webOptions.accessTokenEndpoint, true); 209 | tokenRequest.setRequestHeader('accept', 'application/json'); 210 | if (this.webOptions.sendCacheControlHeader) { 211 | tokenRequest.setRequestHeader( 212 | 'cache-control', 213 | 'no-cache', 214 | ); 215 | } 216 | tokenRequest.setRequestHeader( 217 | 'content-type', 218 | 'application/x-www-form-urlencoded', 219 | ); 220 | tokenRequest.send( 221 | WebUtils.getTokenEndpointData(this.webOptions, authorizationCode), 222 | ); 223 | } 224 | 225 | private requestResource( 226 | accessToken: string, 227 | resolve: any, 228 | reject: (reason?: any) => void, 229 | authorizationResponse: any, 230 | accessTokenResponse: any = null, 231 | ) { 232 | if (this.webOptions.resourceUrl) { 233 | const logsEnabled = this.webOptions.logsEnabled; 234 | if (logsEnabled) { 235 | this.doLog('Resource url: ' + this.webOptions.resourceUrl); 236 | } 237 | if (accessToken) { 238 | if (logsEnabled) { 239 | this.doLog('Access token:', accessToken); 240 | } 241 | const self = this; 242 | const request = new XMLHttpRequest(); 243 | request.onload = function () { 244 | if (this.status === 200) { 245 | const resp = JSON.parse(this.response); 246 | if (logsEnabled) { 247 | self.doLog('Resource response:', resp); 248 | } 249 | if (resp) { 250 | self.assignResponses( 251 | resp, 252 | accessToken, 253 | authorizationResponse, 254 | accessTokenResponse, 255 | ); 256 | } 257 | if (logsEnabled) { 258 | self.doLog(self.MSG_RETURNED_TO_JS, resp); 259 | } 260 | resolve(resp); 261 | } else { 262 | reject(new Error(this.statusText)); 263 | } 264 | self.closeWindow(); 265 | }; 266 | request.onerror = function () { 267 | if (logsEnabled) { 268 | self.doLog('ERR_GENERAL: ' + this.statusText); 269 | } 270 | reject(new Error('ERR_GENERAL')); 271 | self.closeWindow(); 272 | }; 273 | request.open('GET', this.webOptions.resourceUrl, true); 274 | request.setRequestHeader('Authorization', `Bearer ${accessToken}`); 275 | if (this.webOptions.additionalResourceHeaders) { 276 | for (const key in this.webOptions.additionalResourceHeaders) { 277 | request.setRequestHeader( 278 | key, 279 | this.webOptions.additionalResourceHeaders[key], 280 | ); 281 | } 282 | } 283 | request.send(); 284 | } else { 285 | if (logsEnabled) { 286 | this.doLog( 287 | 'No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config.', 288 | ); 289 | } 290 | reject(new Error('ERR_NO_ACCESS_TOKEN')); 291 | this.closeWindow(); 292 | } 293 | } else { 294 | // if no resource url exists just return the accessToken response 295 | const resp = {}; 296 | this.assignResponses( 297 | resp, 298 | accessToken, 299 | authorizationResponse, 300 | accessTokenResponse, 301 | ); 302 | if (this.webOptions.logsEnabled) { 303 | this.doLog(this.MSG_RETURNED_TO_JS, resp); 304 | } 305 | resolve(resp); 306 | this.closeWindow(); 307 | } 308 | } 309 | 310 | assignResponses( 311 | resp: any, 312 | accessToken: string, 313 | authorizationResponse: any, 314 | accessTokenResponse: any = null, 315 | ): void { 316 | // #154 317 | if (authorizationResponse) { 318 | resp['authorization_response'] = authorizationResponse; 319 | } 320 | if (accessTokenResponse) { 321 | resp['access_token_response'] = accessTokenResponse; 322 | } 323 | resp['access_token'] = accessToken; 324 | } 325 | 326 | async logout(options: OAuth2AuthenticateOptions): Promise { 327 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 328 | return new Promise((resolve, _reject) => { 329 | localStorage.removeItem(WebUtils.getAppId(options)); 330 | resolve(true); 331 | }); 332 | } 333 | 334 | private closeWindow() { 335 | window.clearInterval(this.intervalId); 336 | // #164 if the provider's login page is opened in the same tab or window it must not be closed 337 | // if (this.webOptions.windowTarget !== "_self") { 338 | // this.windowHandle?.close(); 339 | // } 340 | this.windowHandle?.close(); 341 | this.windowClosedByPlugin = true; 342 | } 343 | 344 | private doLog(msg: string, obj: any = null) { 345 | console.log('I/Capacitor/GenericOAuth2Plugin: ' + msg, obj); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "inlineSources": true, 7 | "lib": ["dom", "es2017"], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "noFallthroughCasesInSwitch": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "dist/esm", 14 | "pretty": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "strictPropertyInitialization": false, 18 | "target": "es2017" 19 | }, 20 | "files": ["src/index.ts"] 21 | } 22 | --------------------------------------------------------------------------------