├── .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 |
--------------------------------------------------------------------------------