├── .github
└── workflows
│ └── swift.yml
├── .gitignore
├── AuthLibrary.podspec
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Examples
└── GAuth-Apple
│ ├── .gitignore
│ ├── GAuth-Apple.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── GAuth iOS.xcscheme
│ │ └── GAuth macOS.xcscheme
│ ├── GAuth-iOS
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ └── WindowAccessor.swift
│ ├── GAuth-macOS
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Info.plist
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── WindowAccessor.swift
│ └── gauth-macos.entitlements
│ └── Shared
│ ├── ContentView.swift
│ ├── GAuth.swift
│ ├── GAuthDemoApp.swift
│ └── gmailv1.swift
├── LICENSE
├── Makefile
├── Package.swift
├── README.md
├── SECURITY.md
├── Sources
├── Examples
│ ├── GitHub
│ │ ├── GitHub.swift
│ │ └── main.swift
│ ├── Google
│ │ ├── Google.swift
│ │ └── main.swift
│ ├── Meetup
│ │ ├── Meetup.swift
│ │ └── main.swift
│ ├── README.md
│ ├── Spotify
│ │ ├── Spotify.swift
│ │ └── main.swift
│ ├── TokenSource
│ │ └── main.swift
│ └── Twitter
│ │ ├── Twitter.swift
│ │ └── main.swift
├── OAuth1
│ ├── BrowserTokenProvider.swift
│ ├── Connection.swift
│ ├── Token.swift
│ ├── TokenProvider.swift
│ ├── encode.swift
│ └── openURL.swift
├── OAuth2
│ ├── AuthError.swift
│ ├── BrowserTokenProvider.swift
│ ├── Code.swift
│ ├── Connection.swift
│ ├── DefaultTokenProvider.swift
│ ├── FCMTokenProvider
│ │ ├── FCMTokenProvider.swift
│ │ ├── README.md
│ │ └── index.js
│ ├── GoogleCloudMetadataTokenProvider.swift
│ ├── GoogleRefreshTokenProvider.swift
│ ├── PlatformNativeTokenProvider.swift
│ ├── Refresh.swift
│ ├── ServiceAccountTokenProvider
│ │ ├── ASN1.swift
│ │ ├── JWT.swift
│ │ ├── RSA.swift
│ │ └── ServiceAccountTokenProvider.swift
│ ├── Token.swift
│ ├── TokenProvider.swift
│ └── openURL.swift
├── SwiftyBase64
│ ├── Alphabets.swift
│ ├── Base64.swift
│ └── LICENSE
└── TinyHTTPServer
│ └── TinyHTTPServer.swift
├── credentials
├── README.md
├── github.json
├── google.json
├── meetup.json
├── spotify.json
└── twitter.json
└── renovate.json
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: Swift
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 |
11 | build-ubuntu:
12 | runs-on: ubuntu-18.04
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 | - name: Install Swift
17 | run: |
18 | SWIFT_URL=https://swift.org/builds/swift-5.1.1-release/ubuntu1404/swift-5.1.1-RELEASE/swift-5.1.1-RELEASE-ubuntu14.04.tar.gz
19 | curl -fSsL $SWIFT_URL -o swift.tar.gz
20 | sudo tar -xzf swift.tar.gz --strip-components=2 --directory=/usr/local
21 | which swift
22 | - name: Build
23 | run: |
24 | swift package -v resolve
25 | make all
26 |
27 | build-macos:
28 | runs-on: macOS-latest
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v3
32 | - name: Build
33 | run: |
34 | swift package -v resolve
35 | make all
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Package.resolved
2 | .swiftpm
3 | .build
4 |
--------------------------------------------------------------------------------
/AuthLibrary.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint AuthLibrary.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = "AuthLibrary"
11 | s.version = "0.5.0"
12 | s.summary = "Auth client library for Swift command-line tools, cloud services, and apps."
13 | s.swift_version = "5.0"
14 | s.ios.deployment_target = "10.0"
15 |
16 | s.description = <<-DESC
17 | Swift packages that can be used to write command-line tools and cloud services that use OAuth to authenticate and authorize access to remote services.
18 |
19 | Currently these packages support OAuth1 and OAuth2. They are designed to work on OS X systems and on Linux systems that are running in the Google Cloud.
20 |
21 | The CocoaPods distribution supports iOS-based authentication using Google Cloud Service Accounts (only).
22 | DESC
23 |
24 | s.homepage = "https://github.com/google/auth-library-swift"
25 | s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
26 | s.author = "Google"
27 | s.source = { :git => "https://github.com/google/auth-library-swift.git", :tag => s.version.to_s }
28 |
29 | s.source_files =
30 | "Sources/OAuth2/Code.swift",
31 | "Sources/OAuth2/Connection.swift",
32 | "Sources/OAuth2/ServiceAccountTokenProvider/ASN1.swift",
33 | "Sources/OAuth2/ServiceAccountTokenProvider/JWT.swift",
34 | "Sources/OAuth2/ServiceAccountTokenProvider/RSA.swift",
35 | "Sources/OAuth2/ServiceAccountTokenProvider/ServiceAccountTokenProvider.swift",
36 | "Sources/OAuth2/Token.swift",
37 | "Sources/OAuth2/TokenProvider.swift",
38 | "Sources/OAuth2/FCMTokenProvider/FCMTokenProvider.swift"
39 |
40 | s.static_framework = true
41 | s.dependency "CryptoSwift", "~> 1.0.0"
42 | s.dependency "BigInt", "~> 3.1.0"
43 | s.dependency "SwiftyBase64", "~> 1.1.1"
44 | s.dependency "SwiftNIO", "~> 2.32.0"
45 | s.dependency "Firebase/Core"
46 | s.dependency "Firebase/Functions"
47 | s.dependency "Firebase/Auth"
48 | s.dependency "Firebase/Firestore"
49 | end
50 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Code owners file.
2 | # This file controls who is tagged for review for any given pull request.
3 | #
4 | # For syntax help see:
5 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
6 |
7 | * @googleapis/lang-swift
8 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project,
4 | and in the interest of fostering an open and welcoming community,
5 | we pledge to respect all people who contribute through reporting issues,
6 | posting feature requests, updating documentation,
7 | submitting pull requests or patches, and other activities.
8 |
9 | We are committed to making participation in this project
10 | a harassment-free experience for everyone,
11 | regardless of level of experience, gender, gender identity and expression,
12 | sexual orientation, disability, personal appearance,
13 | body size, race, ethnicity, age, religion, or nationality.
14 |
15 | Examples of unacceptable behavior by participants include:
16 |
17 | * The use of sexualized language or imagery
18 | * Personal attacks
19 | * Trolling or insulting/derogatory comments
20 | * Public or private harassment
21 | * Publishing other's private information,
22 | such as physical or electronic
23 | addresses, without explicit permission
24 | * Other unethical or unprofessional conduct.
25 |
26 | Project maintainers have the right and responsibility to remove, edit, or reject
27 | comments, commits, code, wiki edits, issues, and other contributions
28 | that are not aligned to this Code of Conduct.
29 | By adopting this Code of Conduct,
30 | project maintainers commit themselves to fairly and consistently
31 | applying these principles to every aspect of managing this project.
32 | Project maintainers who do not follow or enforce the Code of Conduct
33 | may be permanently removed from the project team.
34 |
35 | This code of conduct applies both within project spaces and in public spaces
36 | when an individual is representing the project or its community.
37 |
38 | Instances of abusive, harassing, or otherwise unacceptable behavior
39 | may be reported by opening an issue
40 | or contacting one or more of the project maintainers.
41 |
42 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
43 | available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
44 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/.gitignore:
--------------------------------------------------------------------------------
1 | ## User settings
2 | xcuserdata/
3 |
4 | ## Code signing
5 | *.cer
6 | *.mobileprovision
7 | *.provisionprofile
8 |
9 | ## App packaging
10 | *.app
11 | *.ipa
12 | *.dSYM.zip
13 | *.dSYM
14 |
15 | ## Playgrounds
16 | timeline.xctimeline
17 | playground.xcworkspace
18 |
19 | # Swift Package Manager
20 | .build/
21 |
22 | # Finder excrement
23 | .DS_Store
24 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-Apple.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 70083F5527556FDB007195C3 /* GAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70083F5427556FDB007195C3 /* GAuth.swift */; };
11 | 70083F5627556FDB007195C3 /* GAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70083F5427556FDB007195C3 /* GAuth.swift */; };
12 | 705A071027445B9800BEF60A /* GoogleAPIRuntime in Frameworks */ = {isa = PBXBuildFile; productRef = 705A070F27445B9800BEF60A /* GoogleAPIRuntime */; };
13 | 705A071227445C0B00BEF60A /* GoogleAPIRuntime in Frameworks */ = {isa = PBXBuildFile; productRef = 705A071127445C0B00BEF60A /* GoogleAPIRuntime */; };
14 | 705A071827471D6B00BEF60A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70988A3927443D2E001F41D6 /* ContentView.swift */; };
15 | 705A071927471DC700BEF60A /* GAuthDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709889F92742F778001F41D6 /* GAuthDemoApp.swift */; };
16 | 705A071D274C184600BEF60A /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A071C274C184600BEF60A /* WindowAccessor.swift */; };
17 | 705A071F274C18CF00BEF60A /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A071E274C18CF00BEF60A /* WindowAccessor.swift */; };
18 | 709889FA2742F778001F41D6 /* GAuthDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709889F92742F778001F41D6 /* GAuthDemoApp.swift */; };
19 | 709889FE2742F77A001F41D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 709889FD2742F77A001F41D6 /* Assets.xcassets */; };
20 | 70988A012742F77A001F41D6 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 70988A002742F77A001F41D6 /* Preview Assets.xcassets */; };
21 | 70988A30274439A9001F41D6 /* gmailv1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70988A082743037E001F41D6 /* gmailv1.swift */; };
22 | 70988A3A27443D2E001F41D6 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70988A3927443D2E001F41D6 /* ContentView.swift */; };
23 | 70988A3C27443D2F001F41D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 70988A3B27443D2F001F41D6 /* Assets.xcassets */; };
24 | 70988A3F27443D2F001F41D6 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 70988A3E27443D2F001F41D6 /* Preview Assets.xcassets */; };
25 | 70988A44274450E5001F41D6 /* gmailv1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70988A082743037E001F41D6 /* gmailv1.swift */; };
26 | /* End PBXBuildFile section */
27 |
28 | /* Begin PBXCopyFilesBuildPhase section */
29 | 70988A2E27441C76001F41D6 /* Embed Frameworks */ = {
30 | isa = PBXCopyFilesBuildPhase;
31 | buildActionMask = 2147483647;
32 | dstPath = "";
33 | dstSubfolderSpec = 10;
34 | files = (
35 | );
36 | name = "Embed Frameworks";
37 | runOnlyForDeploymentPostprocessing = 0;
38 | };
39 | /* End PBXCopyFilesBuildPhase section */
40 |
41 | /* Begin PBXFileReference section */
42 | 70083F53274C5A15007195C3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
43 | 70083F5427556FDB007195C3 /* GAuth.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = GAuth.swift; sourceTree = ""; tabWidth = 2; };
44 | 705A071B27486E7900BEF60A /* .. */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ..; path = ../..; sourceTree = ""; };
45 | 705A071C274C184600BEF60A /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; };
46 | 705A071E274C18CF00BEF60A /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; };
47 | 709889F62742F778001F41D6 /* GAuth-macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "GAuth-macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
48 | 709889F92742F778001F41D6 /* GAuthDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GAuthDemoApp.swift; sourceTree = ""; };
49 | 709889FD2742F77A001F41D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
50 | 70988A002742F77A001F41D6 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
51 | 70988A022742F77A001F41D6 /* gauth-macos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "gauth-macos.entitlements"; sourceTree = ""; };
52 | 70988A082743037E001F41D6 /* gmailv1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = gmailv1.swift; sourceTree = ""; };
53 | 70988A3527443D2E001F41D6 /* GAuth-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "GAuth-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
54 | 70988A3927443D2E001F41D6 /* ContentView.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; tabWidth = 2; };
55 | 70988A3B27443D2F001F41D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
56 | 70988A3E27443D2F001F41D6 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
57 | /* End PBXFileReference section */
58 |
59 | /* Begin PBXFrameworksBuildPhase section */
60 | 709889F32742F778001F41D6 /* Frameworks */ = {
61 | isa = PBXFrameworksBuildPhase;
62 | buildActionMask = 2147483647;
63 | files = (
64 | 705A071227445C0B00BEF60A /* GoogleAPIRuntime in Frameworks */,
65 | );
66 | runOnlyForDeploymentPostprocessing = 0;
67 | };
68 | 70988A3227443D2E001F41D6 /* Frameworks */ = {
69 | isa = PBXFrameworksBuildPhase;
70 | buildActionMask = 2147483647;
71 | files = (
72 | 705A071027445B9800BEF60A /* GoogleAPIRuntime in Frameworks */,
73 | );
74 | runOnlyForDeploymentPostprocessing = 0;
75 | };
76 | /* End PBXFrameworksBuildPhase section */
77 |
78 | /* Begin PBXGroup section */
79 | 705A071A27486E7900BEF60A /* Packages */ = {
80 | isa = PBXGroup;
81 | children = (
82 | 705A071B27486E7900BEF60A /* .. */,
83 | );
84 | name = Packages;
85 | sourceTree = "";
86 | };
87 | 709889ED2742F778001F41D6 = {
88 | isa = PBXGroup;
89 | children = (
90 | 705A071A27486E7900BEF60A /* Packages */,
91 | 70988A43274450D8001F41D6 /* Shared */,
92 | 709889F82742F778001F41D6 /* GAuth-macOS */,
93 | 70988A3627443D2E001F41D6 /* GAuth-iOS */,
94 | 709889F72742F778001F41D6 /* Products */,
95 | 70988A4527445258001F41D6 /* Frameworks */,
96 | );
97 | sourceTree = "";
98 | };
99 | 709889F72742F778001F41D6 /* Products */ = {
100 | isa = PBXGroup;
101 | children = (
102 | 709889F62742F778001F41D6 /* GAuth-macOS.app */,
103 | 70988A3527443D2E001F41D6 /* GAuth-iOS.app */,
104 | );
105 | name = Products;
106 | sourceTree = "";
107 | };
108 | 709889F82742F778001F41D6 /* GAuth-macOS */ = {
109 | isa = PBXGroup;
110 | children = (
111 | 70083F53274C5A15007195C3 /* Info.plist */,
112 | 709889FD2742F77A001F41D6 /* Assets.xcassets */,
113 | 70988A022742F77A001F41D6 /* gauth-macos.entitlements */,
114 | 709889FF2742F77A001F41D6 /* Preview Content */,
115 | 705A071E274C18CF00BEF60A /* WindowAccessor.swift */,
116 | );
117 | path = "GAuth-macOS";
118 | sourceTree = "";
119 | };
120 | 709889FF2742F77A001F41D6 /* Preview Content */ = {
121 | isa = PBXGroup;
122 | children = (
123 | 70988A002742F77A001F41D6 /* Preview Assets.xcassets */,
124 | );
125 | path = "Preview Content";
126 | sourceTree = "";
127 | };
128 | 70988A3627443D2E001F41D6 /* GAuth-iOS */ = {
129 | isa = PBXGroup;
130 | children = (
131 | 70988A3B27443D2F001F41D6 /* Assets.xcassets */,
132 | 70988A3D27443D2F001F41D6 /* Preview Content */,
133 | 705A071C274C184600BEF60A /* WindowAccessor.swift */,
134 | );
135 | path = "GAuth-iOS";
136 | sourceTree = "";
137 | };
138 | 70988A3D27443D2F001F41D6 /* Preview Content */ = {
139 | isa = PBXGroup;
140 | children = (
141 | 70988A3E27443D2F001F41D6 /* Preview Assets.xcassets */,
142 | );
143 | path = "Preview Content";
144 | sourceTree = "";
145 | };
146 | 70988A43274450D8001F41D6 /* Shared */ = {
147 | isa = PBXGroup;
148 | children = (
149 | 70988A3927443D2E001F41D6 /* ContentView.swift */,
150 | 709889F92742F778001F41D6 /* GAuthDemoApp.swift */,
151 | 70988A082743037E001F41D6 /* gmailv1.swift */,
152 | 70083F5427556FDB007195C3 /* GAuth.swift */,
153 | );
154 | path = Shared;
155 | sourceTree = "";
156 | };
157 | 70988A4527445258001F41D6 /* Frameworks */ = {
158 | isa = PBXGroup;
159 | children = (
160 | );
161 | name = Frameworks;
162 | sourceTree = "";
163 | };
164 | /* End PBXGroup section */
165 |
166 | /* Begin PBXNativeTarget section */
167 | 709889F52742F778001F41D6 /* GAuth-macOS */ = {
168 | isa = PBXNativeTarget;
169 | buildConfigurationList = 70988A052742F77A001F41D6 /* Build configuration list for PBXNativeTarget "GAuth-macOS" */;
170 | buildPhases = (
171 | 709889F22742F778001F41D6 /* Sources */,
172 | 709889F32742F778001F41D6 /* Frameworks */,
173 | 709889F42742F778001F41D6 /* Resources */,
174 | 70988A2E27441C76001F41D6 /* Embed Frameworks */,
175 | );
176 | buildRules = (
177 | );
178 | dependencies = (
179 | );
180 | name = "GAuth-macOS";
181 | packageProductDependencies = (
182 | 705A071127445C0B00BEF60A /* GoogleAPIRuntime */,
183 | );
184 | productName = "gmail-test";
185 | productReference = 709889F62742F778001F41D6 /* GAuth-macOS.app */;
186 | productType = "com.apple.product-type.application";
187 | };
188 | 70988A3427443D2E001F41D6 /* GAuth-iOS */ = {
189 | isa = PBXNativeTarget;
190 | buildConfigurationList = 70988A4027443D2F001F41D6 /* Build configuration list for PBXNativeTarget "GAuth-iOS" */;
191 | buildPhases = (
192 | 70988A3127443D2E001F41D6 /* Sources */,
193 | 70988A3227443D2E001F41D6 /* Frameworks */,
194 | 70988A3327443D2E001F41D6 /* Resources */,
195 | );
196 | buildRules = (
197 | );
198 | dependencies = (
199 | );
200 | name = "GAuth-iOS";
201 | packageProductDependencies = (
202 | 705A070F27445B9800BEF60A /* GoogleAPIRuntime */,
203 | );
204 | productName = "Gmail iOS";
205 | productReference = 70988A3527443D2E001F41D6 /* GAuth-iOS.app */;
206 | productType = "com.apple.product-type.application";
207 | };
208 | /* End PBXNativeTarget section */
209 |
210 | /* Begin PBXProject section */
211 | 709889EE2742F778001F41D6 /* Project object */ = {
212 | isa = PBXProject;
213 | attributes = {
214 | BuildIndependentTargetsInParallel = 1;
215 | LastSwiftUpdateCheck = 1310;
216 | LastUpgradeCheck = 1310;
217 | TargetAttributes = {
218 | 709889F52742F778001F41D6 = {
219 | CreatedOnToolsVersion = 13.1;
220 | };
221 | 70988A3427443D2E001F41D6 = {
222 | CreatedOnToolsVersion = 13.1;
223 | };
224 | };
225 | };
226 | buildConfigurationList = 709889F12742F778001F41D6 /* Build configuration list for PBXProject "GAuth-Apple" */;
227 | compatibilityVersion = "Xcode 13.0";
228 | developmentRegion = en;
229 | hasScannedForEncodings = 0;
230 | knownRegions = (
231 | en,
232 | Base,
233 | );
234 | mainGroup = 709889ED2742F778001F41D6;
235 | packageReferences = (
236 | 705A070E27445B9800BEF60A /* XCRemoteSwiftPackageReference "google-api-swift-client" */,
237 | );
238 | productRefGroup = 709889F72742F778001F41D6 /* Products */;
239 | projectDirPath = "";
240 | projectRoot = "";
241 | targets = (
242 | 709889F52742F778001F41D6 /* GAuth-macOS */,
243 | 70988A3427443D2E001F41D6 /* GAuth-iOS */,
244 | );
245 | };
246 | /* End PBXProject section */
247 |
248 | /* Begin PBXResourcesBuildPhase section */
249 | 709889F42742F778001F41D6 /* Resources */ = {
250 | isa = PBXResourcesBuildPhase;
251 | buildActionMask = 2147483647;
252 | files = (
253 | 70988A012742F77A001F41D6 /* Preview Assets.xcassets in Resources */,
254 | 709889FE2742F77A001F41D6 /* Assets.xcassets in Resources */,
255 | );
256 | runOnlyForDeploymentPostprocessing = 0;
257 | };
258 | 70988A3327443D2E001F41D6 /* Resources */ = {
259 | isa = PBXResourcesBuildPhase;
260 | buildActionMask = 2147483647;
261 | files = (
262 | 70988A3F27443D2F001F41D6 /* Preview Assets.xcassets in Resources */,
263 | 70988A3C27443D2F001F41D6 /* Assets.xcassets in Resources */,
264 | );
265 | runOnlyForDeploymentPostprocessing = 0;
266 | };
267 | /* End PBXResourcesBuildPhase section */
268 |
269 | /* Begin PBXSourcesBuildPhase section */
270 | 709889F22742F778001F41D6 /* Sources */ = {
271 | isa = PBXSourcesBuildPhase;
272 | buildActionMask = 2147483647;
273 | files = (
274 | 70988A30274439A9001F41D6 /* gmailv1.swift in Sources */,
275 | 705A071827471D6B00BEF60A /* ContentView.swift in Sources */,
276 | 705A071F274C18CF00BEF60A /* WindowAccessor.swift in Sources */,
277 | 709889FA2742F778001F41D6 /* GAuthDemoApp.swift in Sources */,
278 | 70083F5527556FDB007195C3 /* GAuth.swift in Sources */,
279 | );
280 | runOnlyForDeploymentPostprocessing = 0;
281 | };
282 | 70988A3127443D2E001F41D6 /* Sources */ = {
283 | isa = PBXSourcesBuildPhase;
284 | buildActionMask = 2147483647;
285 | files = (
286 | 70988A44274450E5001F41D6 /* gmailv1.swift in Sources */,
287 | 705A071927471DC700BEF60A /* GAuthDemoApp.swift in Sources */,
288 | 705A071D274C184600BEF60A /* WindowAccessor.swift in Sources */,
289 | 70988A3A27443D2E001F41D6 /* ContentView.swift in Sources */,
290 | 70083F5627556FDB007195C3 /* GAuth.swift in Sources */,
291 | );
292 | runOnlyForDeploymentPostprocessing = 0;
293 | };
294 | /* End PBXSourcesBuildPhase section */
295 |
296 | /* Begin XCBuildConfiguration section */
297 | 70988A032742F77A001F41D6 /* Debug */ = {
298 | isa = XCBuildConfiguration;
299 | buildSettings = {
300 | ALWAYS_SEARCH_USER_PATHS = NO;
301 | CLANG_ANALYZER_NONNULL = YES;
302 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
304 | CLANG_CXX_LIBRARY = "libc++";
305 | CLANG_ENABLE_MODULES = YES;
306 | CLANG_ENABLE_OBJC_ARC = YES;
307 | CLANG_ENABLE_OBJC_WEAK = YES;
308 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
309 | CLANG_WARN_BOOL_CONVERSION = YES;
310 | CLANG_WARN_COMMA = YES;
311 | CLANG_WARN_CONSTANT_CONVERSION = YES;
312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
313 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
314 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
315 | CLANG_WARN_EMPTY_BODY = YES;
316 | CLANG_WARN_ENUM_CONVERSION = YES;
317 | CLANG_WARN_INFINITE_RECURSION = YES;
318 | CLANG_WARN_INT_CONVERSION = YES;
319 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
320 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
321 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
323 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
324 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
325 | CLANG_WARN_STRICT_PROTOTYPES = YES;
326 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
327 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
328 | CLANG_WARN_UNREACHABLE_CODE = YES;
329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
330 | COPY_PHASE_STRIP = NO;
331 | DEBUG_INFORMATION_FORMAT = dwarf;
332 | ENABLE_STRICT_OBJC_MSGSEND = YES;
333 | ENABLE_TESTABILITY = YES;
334 | GCC_C_LANGUAGE_STANDARD = gnu11;
335 | GCC_DYNAMIC_NO_PIC = NO;
336 | GCC_NO_COMMON_BLOCKS = YES;
337 | GCC_OPTIMIZATION_LEVEL = 0;
338 | GCC_PREPROCESSOR_DEFINITIONS = (
339 | "DEBUG=1",
340 | "$(inherited)",
341 | );
342 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
343 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
344 | GCC_WARN_UNDECLARED_SELECTOR = YES;
345 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
346 | GCC_WARN_UNUSED_FUNCTION = YES;
347 | GCC_WARN_UNUSED_VARIABLE = YES;
348 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
349 | MACOSX_DEPLOYMENT_TARGET = 12.0;
350 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
351 | MTL_FAST_MATH = YES;
352 | ONLY_ACTIVE_ARCH = YES;
353 | SDKROOT = macosx;
354 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
355 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
356 | };
357 | name = Debug;
358 | };
359 | 70988A042742F77A001F41D6 /* Release */ = {
360 | isa = XCBuildConfiguration;
361 | buildSettings = {
362 | ALWAYS_SEARCH_USER_PATHS = NO;
363 | CLANG_ANALYZER_NONNULL = YES;
364 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
365 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
366 | CLANG_CXX_LIBRARY = "libc++";
367 | CLANG_ENABLE_MODULES = YES;
368 | CLANG_ENABLE_OBJC_ARC = YES;
369 | CLANG_ENABLE_OBJC_WEAK = YES;
370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
371 | CLANG_WARN_BOOL_CONVERSION = YES;
372 | CLANG_WARN_COMMA = YES;
373 | CLANG_WARN_CONSTANT_CONVERSION = YES;
374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
375 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
376 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
377 | CLANG_WARN_EMPTY_BODY = YES;
378 | CLANG_WARN_ENUM_CONVERSION = YES;
379 | CLANG_WARN_INFINITE_RECURSION = YES;
380 | CLANG_WARN_INT_CONVERSION = YES;
381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
385 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
386 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
387 | CLANG_WARN_STRICT_PROTOTYPES = YES;
388 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
389 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
390 | CLANG_WARN_UNREACHABLE_CODE = YES;
391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
392 | COPY_PHASE_STRIP = NO;
393 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
394 | ENABLE_NS_ASSERTIONS = NO;
395 | ENABLE_STRICT_OBJC_MSGSEND = YES;
396 | GCC_C_LANGUAGE_STANDARD = gnu11;
397 | GCC_NO_COMMON_BLOCKS = YES;
398 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
399 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
400 | GCC_WARN_UNDECLARED_SELECTOR = YES;
401 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
402 | GCC_WARN_UNUSED_FUNCTION = YES;
403 | GCC_WARN_UNUSED_VARIABLE = YES;
404 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
405 | MACOSX_DEPLOYMENT_TARGET = 12.0;
406 | MTL_ENABLE_DEBUG_INFO = NO;
407 | MTL_FAST_MATH = YES;
408 | SDKROOT = macosx;
409 | SWIFT_COMPILATION_MODE = wholemodule;
410 | SWIFT_OPTIMIZATION_LEVEL = "-O";
411 | };
412 | name = Release;
413 | };
414 | 70988A062742F77A001F41D6 /* Debug */ = {
415 | isa = XCBuildConfiguration;
416 | buildSettings = {
417 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
418 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
419 | CODE_SIGN_ENTITLEMENTS = "GAuth-macOS/gauth-macos.entitlements";
420 | CODE_SIGN_IDENTITY = "-";
421 | CODE_SIGN_STYLE = Automatic;
422 | COMBINE_HIDPI_IMAGES = YES;
423 | CURRENT_PROJECT_VERSION = 1;
424 | DEVELOPMENT_ASSET_PATHS = "GAuth-macOS/Preview\\ Content/Preview\\ Assets.xcassets";
425 | DEVELOPMENT_TEAM = "";
426 | ENABLE_HARDENED_RUNTIME = YES;
427 | ENABLE_PREVIEWS = YES;
428 | GENERATE_INFOPLIST_FILE = YES;
429 | INFOPLIST_FILE = "GAuth-macOS/Info.plist";
430 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
431 | LD_RUNPATH_SEARCH_PATHS = (
432 | "$(inherited)",
433 | "@executable_path/../Frameworks",
434 | );
435 | MACOSX_DEPLOYMENT_TARGET = 11.0;
436 | MARKETING_VERSION = 1.0;
437 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.gauth-macos";
438 | PRODUCT_NAME = "$(TARGET_NAME)";
439 | SWIFT_EMIT_LOC_STRINGS = YES;
440 | SWIFT_VERSION = 5.0;
441 | };
442 | name = Debug;
443 | };
444 | 70988A072742F77A001F41D6 /* Release */ = {
445 | isa = XCBuildConfiguration;
446 | buildSettings = {
447 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
448 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
449 | CODE_SIGN_ENTITLEMENTS = "GAuth-macOS/gauth-macos.entitlements";
450 | CODE_SIGN_IDENTITY = "-";
451 | CODE_SIGN_STYLE = Automatic;
452 | COMBINE_HIDPI_IMAGES = YES;
453 | CURRENT_PROJECT_VERSION = 1;
454 | DEVELOPMENT_ASSET_PATHS = "GAuth-macOS/Preview\\ Content/Preview\\ Assets.xcassets";
455 | DEVELOPMENT_TEAM = "";
456 | ENABLE_HARDENED_RUNTIME = YES;
457 | ENABLE_PREVIEWS = YES;
458 | GENERATE_INFOPLIST_FILE = YES;
459 | INFOPLIST_FILE = "GAuth-macOS/Info.plist";
460 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
461 | LD_RUNPATH_SEARCH_PATHS = (
462 | "$(inherited)",
463 | "@executable_path/../Frameworks",
464 | );
465 | MACOSX_DEPLOYMENT_TARGET = 11.0;
466 | MARKETING_VERSION = 1.0;
467 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.gauth-macos";
468 | PRODUCT_NAME = "$(TARGET_NAME)";
469 | SWIFT_EMIT_LOC_STRINGS = YES;
470 | SWIFT_VERSION = 5.0;
471 | };
472 | name = Release;
473 | };
474 | 70988A4127443D2F001F41D6 /* Debug */ = {
475 | isa = XCBuildConfiguration;
476 | buildSettings = {
477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
478 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
479 | CODE_SIGN_STYLE = Automatic;
480 | CURRENT_PROJECT_VERSION = 1;
481 | DEVELOPMENT_ASSET_PATHS = "GAuth-iOS/Preview\\ Content/Preview\\ Assets.xcassets";
482 | DEVELOPMENT_TEAM = "";
483 | ENABLE_PREVIEWS = YES;
484 | GENERATE_INFOPLIST_FILE = YES;
485 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
486 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
487 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
488 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
489 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
490 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
491 | LD_RUNPATH_SEARCH_PATHS = (
492 | "$(inherited)",
493 | "@executable_path/Frameworks",
494 | );
495 | MARKETING_VERSION = 1.0;
496 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.gauth-ios";
497 | PRODUCT_NAME = "$(TARGET_NAME)";
498 | SDKROOT = iphoneos;
499 | SWIFT_EMIT_LOC_STRINGS = YES;
500 | SWIFT_VERSION = 5.0;
501 | TARGETED_DEVICE_FAMILY = "1,2";
502 | };
503 | name = Debug;
504 | };
505 | 70988A4227443D2F001F41D6 /* Release */ = {
506 | isa = XCBuildConfiguration;
507 | buildSettings = {
508 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
509 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
510 | CODE_SIGN_STYLE = Automatic;
511 | CURRENT_PROJECT_VERSION = 1;
512 | DEVELOPMENT_ASSET_PATHS = "GAuth-iOS/Preview\\ Content/Preview\\ Assets.xcassets";
513 | DEVELOPMENT_TEAM = "";
514 | ENABLE_PREVIEWS = YES;
515 | GENERATE_INFOPLIST_FILE = YES;
516 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
517 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
518 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
519 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
520 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
521 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
522 | LD_RUNPATH_SEARCH_PATHS = (
523 | "$(inherited)",
524 | "@executable_path/Frameworks",
525 | );
526 | MARKETING_VERSION = 1.0;
527 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.gauth-ios";
528 | PRODUCT_NAME = "$(TARGET_NAME)";
529 | SDKROOT = iphoneos;
530 | SWIFT_EMIT_LOC_STRINGS = YES;
531 | SWIFT_VERSION = 5.0;
532 | TARGETED_DEVICE_FAMILY = "1,2";
533 | VALIDATE_PRODUCT = YES;
534 | };
535 | name = Release;
536 | };
537 | /* End XCBuildConfiguration section */
538 |
539 | /* Begin XCConfigurationList section */
540 | 709889F12742F778001F41D6 /* Build configuration list for PBXProject "GAuth-Apple" */ = {
541 | isa = XCConfigurationList;
542 | buildConfigurations = (
543 | 70988A032742F77A001F41D6 /* Debug */,
544 | 70988A042742F77A001F41D6 /* Release */,
545 | );
546 | defaultConfigurationIsVisible = 0;
547 | defaultConfigurationName = Release;
548 | };
549 | 70988A052742F77A001F41D6 /* Build configuration list for PBXNativeTarget "GAuth-macOS" */ = {
550 | isa = XCConfigurationList;
551 | buildConfigurations = (
552 | 70988A062742F77A001F41D6 /* Debug */,
553 | 70988A072742F77A001F41D6 /* Release */,
554 | );
555 | defaultConfigurationIsVisible = 0;
556 | defaultConfigurationName = Release;
557 | };
558 | 70988A4027443D2F001F41D6 /* Build configuration list for PBXNativeTarget "GAuth-iOS" */ = {
559 | isa = XCConfigurationList;
560 | buildConfigurations = (
561 | 70988A4127443D2F001F41D6 /* Debug */,
562 | 70988A4227443D2F001F41D6 /* Release */,
563 | );
564 | defaultConfigurationIsVisible = 0;
565 | defaultConfigurationName = Release;
566 | };
567 | /* End XCConfigurationList section */
568 |
569 | /* Begin XCRemoteSwiftPackageReference section */
570 | 705A070E27445B9800BEF60A /* XCRemoteSwiftPackageReference "google-api-swift-client" */ = {
571 | isa = XCRemoteSwiftPackageReference;
572 | repositoryURL = "https://github.com/googleapis/google-api-swift-client";
573 | requirement = {
574 | branch = main;
575 | kind = branch;
576 | };
577 | };
578 | /* End XCRemoteSwiftPackageReference section */
579 |
580 | /* Begin XCSwiftPackageProductDependency section */
581 | 705A070F27445B9800BEF60A /* GoogleAPIRuntime */ = {
582 | isa = XCSwiftPackageProductDependency;
583 | package = 705A070E27445B9800BEF60A /* XCRemoteSwiftPackageReference "google-api-swift-client" */;
584 | productName = GoogleAPIRuntime;
585 | };
586 | 705A071127445C0B00BEF60A /* GoogleAPIRuntime */ = {
587 | isa = XCSwiftPackageProductDependency;
588 | package = 705A070E27445B9800BEF60A /* XCRemoteSwiftPackageReference "google-api-swift-client" */;
589 | productName = GoogleAPIRuntime;
590 | };
591 | /* End XCSwiftPackageProductDependency section */
592 | };
593 | rootObject = 709889EE2742F778001F41D6 /* Project object */;
594 | }
595 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-Apple.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-Apple.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-Apple.xcodeproj/xcshareddata/xcschemes/GAuth iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-Apple.xcodeproj/xcshareddata/xcschemes/GAuth macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-iOS/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-iOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-iOS/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-iOS/WindowAccessor.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import SwiftUI
16 |
17 | struct WindowAccessor: UIViewRepresentable {
18 | @Binding var window: UIWindow?
19 |
20 | func makeUIView(context: Context) -> UIView {
21 | let view = UIView()
22 | DispatchQueue.main.async {
23 | self.window = view.window // << right after inserted in window
24 | }
25 | return view
26 | }
27 |
28 | func updateUIView(_ uiView: UIView, context: Context) {}
29 | }
30 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-macOS/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-macOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-macOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-macOS/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-macOS/WindowAccessor.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import SwiftUI
16 |
17 | struct WindowAccessor: NSViewRepresentable {
18 | @Binding var window: NSWindow?
19 |
20 | func makeNSView(context: Context) -> NSView {
21 | let view = NSView()
22 | DispatchQueue.main.async {
23 | self.window = view.window // << right after inserted in window
24 | }
25 | return view
26 | }
27 |
28 | func updateNSView(_ nsView: NSView, context: Context) {}
29 | }
30 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/GAuth-macOS/gauth-macos.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 | com.apple.security.network.client
10 |
11 | com.apple.security.network.server
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/Shared/ContentView.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import AuthenticationServices
16 | import SwiftUI
17 |
18 | struct ContentView: View {
19 |
20 | @State private var window: ASPresentationAnchor?
21 |
22 | var body: some View {
23 | VStack {
24 | HStack { Spacer().frame(maxWidth: .infinity) }
25 | Text("GAuth Example")
26 | Divider()
27 |
28 | VStack {
29 | Text("Browser")
30 | Button {
31 | oauth2_browser()
32 | } label: {
33 | Text("Login")
34 | }
35 | Button {
36 | token_refresh(.native)
37 | } label: {
38 | Text("Refresh Token")
39 | }
40 | Button {
41 | query_gmail(.native)
42 | } label: {
43 | Text("Query Gmail")
44 | }
45 | Button {
46 | token_cache_clear(.native)
47 | } label: {
48 | Text("Clear Token Cache")
49 | }
50 | }
51 |
52 | Divider()
53 | .padding()
54 | .frame(maxWidth: 120)
55 |
56 | VStack {
57 | Text("Native")
58 | Button {
59 | oauth2_native(anchor: window!)
60 | } label: {
61 | Text("Login")
62 | }
63 | Button {
64 | token_refresh(.native)
65 | } label: {
66 | Text("Refresh Token")
67 | }
68 | Button {
69 | query_gmail(.native)
70 | } label: {
71 | Text("Query Gmail")
72 | }
73 | Button {
74 | token_cache_clear(.native)
75 | } label: {
76 | Text("Clear Token Cache")
77 | }
78 | }
79 | Spacer()
80 | }
81 | .padding()
82 | .background(WindowAccessor(window: $window))
83 | }
84 | }
85 |
86 | // MARK: previews
87 |
88 | struct ContentView_Previews: PreviewProvider {
89 | static var previews: some View {
90 | ContentView()
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/Shared/GAuth.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import AuthenticationServices
16 | import Foundation
17 | import OAuth2
18 |
19 | // MARK: setup overview
20 |
21 | // You'll want to create a new demo/example project in the cloud console. Then,
22 | // navigate to the cloud console api credentials page and configure the consent
23 | // screen. A workspace internal screen will get you going quickly and does not
24 | // require review, however you'll only be able to use your own workspace email
25 | // addresses to complete the oauth flow until you configure an external screen.
26 | //
27 | // https://console.cloud.google.com/apis/credentials
28 | //
29 | // Create an OAuth client ID crediential. An iOS credential works fine for both
30 | // these example flows (the browser flow doesn't need a "browser" credential).
31 | //
32 | // Plug the correct credential values into the inline JSON below. You
33 | // can copy the correct values from the console UI or download the credential
34 | // file and take the values from there.
35 | //
36 | // Enable the appropriate API for your project in the cloud console. This
37 | // example uses the Gmail API to get a list of your 50 most recent message IDs.
38 | //
39 | // https://console.cloud.google.com/apis/library/gmail.googleapis.com
40 | //
41 | // Review the Google OAuth2 overview for further details:
42 | //
43 | // https://developers.google.com/identity/protocols/oauth2
44 |
45 | // MARK: oauth2 browser
46 |
47 | #error("add OAuth2 from cloud console api credentials page")
48 | let creds_browser = """
49 | {
50 | "client_id": "",
51 | "client_secret": "",
52 | "authorize_url": "https://accounts.google.com/o/oauth2/auth",
53 | "access_token_url": "https://accounts.google.com/o/oauth2/token",
54 | "callback": "/google/callback"
55 | }
56 | """
57 |
58 | func browser_provider() -> BrowserTokenProvider {
59 | let cdata = creds_browser.data(using: .utf8)!
60 | let tfile = token_cache_url(.browser)?.path ?? ""
61 | guard let tp = BrowserTokenProvider(credentials: cdata, token: tfile)
62 | else {
63 | fatalError("error creating browser token provider")
64 | }
65 | return tp
66 | }
67 |
68 | func oauth2_browser() {
69 | let tp = browser_provider()
70 |
71 | if tp.token != nil {
72 | print("using existing token")
73 | return
74 | }
75 |
76 | print("requesting new token")
77 | do {
78 | let readonly = "https://www.googleapis.com/auth/gmail.readonly"
79 | try tp.signIn(scopes: [readonly])
80 | } catch let e {
81 | print("error signing in: \(e)")
82 | return
83 | }
84 | try! tp.saveToken(token_cache_url(.browser)!.path)
85 | print("token saved")
86 | }
87 |
88 | // MARK: oauth2 native
89 |
90 | #error("add OAuth2 and from cloud console api credentials page")
91 | let creds_native = """
92 | {
93 | "client_id": "",
94 | "authorize_url": "https://accounts.google.com/o/oauth2/auth",
95 | "access_token_url": "https://accounts.google.com/o/oauth2/token",
96 | "callback_scheme": ""
97 | }
98 | """
99 |
100 | func native_provider() -> PlatformNativeTokenProvider {
101 | let cdata = creds_native.data(using: .utf8)!
102 | let tfile = token_cache_url(.native)?.path ?? ""
103 | guard let tp = PlatformNativeTokenProvider(credentials: cdata, token: tfile)
104 | else {
105 | fatalError("error creating native token provider")
106 | }
107 | return tp
108 | }
109 |
110 | func oauth2_native(anchor a: ASPresentationAnchor) {
111 | let tp = native_provider()
112 |
113 | if tp.token != nil {
114 | print("using existing token")
115 | return
116 | }
117 |
118 | print("requesting new token")
119 | let ctx = AuthContext(anchor: a)
120 | let readonly = "https://www.googleapis.com/auth/gmail.readonly"
121 | tp.signIn(scopes: [readonly], context: ctx) {
122 | if $0 != nil { // token
123 | try! tp.saveToken(token_cache_url(.native)!.path)
124 | print("token saved")
125 | } else {
126 | print("error signing in: \($1!)")
127 | }
128 | }
129 | }
130 |
131 | class AuthContext: NSObject, ASWebAuthenticationPresentationContextProviding {
132 | let anchor: ASPresentationAnchor
133 | init(anchor: ASPresentationAnchor) {
134 | self.anchor = anchor
135 | }
136 | func presentationAnchor(for session: ASWebAuthenticationSession)
137 | -> ASPresentationAnchor {
138 | return self.anchor
139 | }
140 | }
141 |
142 | // MARK: general
143 |
144 | enum FlowType: String {
145 | case browser = "browser"
146 | case native = "native"
147 | }
148 |
149 | func token_cache_url(_ flow: FlowType) -> URL? {
150 | FileManager.default
151 | .urls(for: .cachesDirectory, in: .userDomainMask).first?
152 | .appendingPathComponent("gauth-token-\(flow.rawValue).json")
153 | }
154 |
155 | func token_cache_clear(_ flow: FlowType) {
156 | if let url = token_cache_url(flow) {
157 | try? FileManager.default.removeItem(at: url)
158 | }
159 | }
160 |
161 | func token_refresh(_ flow: FlowType) {
162 | switch flow {
163 | case .browser:
164 | let tp = browser_provider()
165 | try! tp.refreshToken(token_cache_url(.browser)!.path)
166 | case .native:
167 | let tp = native_provider()
168 | try! tp.refreshToken(token_cache_url(.native)!.path)
169 | }
170 | print("token refreshed")
171 | }
172 |
173 | // MARK: query gmail
174 |
175 | func query_gmail(_ flow: FlowType) {
176 | switch flow {
177 | case .browser:
178 | let tp = browser_provider()
179 | query_gmail(tp)
180 | case .native:
181 | let tp = native_provider()
182 | query_gmail(tp)
183 | }
184 | }
185 |
186 | func query_gmail(_ tp: TokenProvider) {
187 | print("Listing Gmail Messages")
188 | let g = Gmail(tokenProvider: tp)
189 | do {
190 | let params = Gmail.users_messageslistParameters(
191 | includeSpamTrash: false,
192 | labelIds: nil,
193 | maxResults: 50,
194 | pageToken: nil,
195 | q: nil,
196 | userId: "me"
197 | )
198 | try g.users_messages_list(parameters: params) { resp, err in
199 | resp.map { print("Response: \n\($0)") }
200 | err.map { print("Error: \n\($0)") }
201 | }
202 | } catch let e {
203 | print("error listing messages: \(e)")
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/Examples/GAuth-Apple/Shared/GAuthDemoApp.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import SwiftUI
16 |
17 | @main
18 | struct GAuthDemoApp: App {
19 | var body: some Scene {
20 | WindowGroup {
21 | ContentView()
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
204 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | all:
3 | swift build -c debug
4 |
5 | project:
6 | swift package generate-xcodeproj
7 |
8 | clean:
9 | rm -rf .build Package.pins Package.resolved
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 |
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 |
17 | import PackageDescription
18 |
19 | let package = Package(
20 | name: "Auth",
21 | platforms: [
22 | .macOS(.v10_12), .iOS(.v9), .tvOS(.v9)
23 | ],
24 | products: [
25 | .library(name: "OAuth1", targets: ["OAuth1"]),
26 | .library(name: "OAuth2", targets: ["OAuth2"]),
27 | .library(name: "TinyHTTPServer", targets: ["TinyHTTPServer"]),
28 | .library(name: "SwiftyBase64", targets: ["SwiftyBase64"]),
29 | ],
30 | dependencies: [
31 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.59.0"),
32 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", "1.1.3"..."1.3.2"),
33 | .package(url: "https://github.com/attaswift/BigInt", from: "5.0.0"),
34 | ],
35 | targets: [
36 | .target(name: "OAuth1",
37 | dependencies: ["CryptoSwift", "TinyHTTPServer"]),
38 | .target(name: "OAuth2",
39 | dependencies: ["CryptoSwift", "TinyHTTPServer", "BigInt", "SwiftyBase64"],
40 | exclude: ["FCMTokenProvider"]),
41 | .target(name: "TinyHTTPServer",
42 | dependencies: ["NIO", "NIOHTTP1"]),
43 | .target(name: "SwiftyBase64"),
44 | .target(name: "TokenSource", dependencies: ["OAuth2"], path: "Sources/Examples/TokenSource"),
45 | .target(name: "Google", dependencies: ["OAuth2"], path: "Sources/Examples/Google"),
46 | .target(name: "GitHub", dependencies: ["OAuth2"], path: "Sources/Examples/GitHub"),
47 | .target(name: "Meetup", dependencies: ["OAuth2"], path: "Sources/Examples/Meetup"),
48 | .target(name: "Spotify", dependencies: ["OAuth2"], path: "Sources/Examples/Spotify"),
49 | .target(name: "Twitter", dependencies: ["OAuth1"], path: "Sources/Examples/Twitter"),
50 | ]
51 | )
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/googleapis/google-auth-library-swift/actions)
2 |
3 | # Auth Library for Swift
4 |
5 | This project contains Swift packages that can be used to write command-line
6 | tools and cloud services that use OAuth to authenticate and authorize access
7 | to remote services.
8 |
9 | Currently these packages support OAuth1 and OAuth2.
10 | They are designed to work on macOS systems and on Linux systems that are
11 | running in the [Google Cloud](https://cloud.google.com).
12 |
13 | * On macOS systems, OAuth tokens can be obtained using the locally-installed
14 | browser and a local web server that is automatically run in the
15 | command-line client.
16 |
17 | * On Linux systems, OAuth tokens can be obtained automatically from the
18 | [Google Cloud Metadata Service](https://cloud.google.com/compute/docs/storing-retrieving-metadata).
19 |
20 | * On both Linux and macOS systems, OAuth tokens can be obtained automatically for
21 | [Google Cloud Service Accounts](https://cloud.google.com/iam/docs/understanding-service-accounts).
22 |
23 | ## Usage and Examples
24 |
25 | [Sources/Examples](Sources/Examples)
26 | contains examples that illustrate OAuth1 and OAuth2 signin for
27 | various services. Each requires valid application credentials to run.
28 | See the various service providers for details.
29 |
30 | The BrowserTokenProvider classes use a local web server to implement
31 | "three-legged OAuth" signin in which users grant permission in a browser
32 | that a provider's server redirects to the client server with a code.
33 | These providers look for OAuth configuration information in "credentials"
34 | YAML files that are expected to be in `$HOME/.credentials`. Sample credentials
35 | files are in [credentials](credentials)
36 | and include client IDs, client secrets, and OAuth service URLs.
37 | When OAuth services require registered callback URLs, these should be
38 | set to `http://localhost:8080/SERVICE/callback` where `SERVICE` is
39 | specified in the corresponding credentials YAML file. The temporary
40 | web server runs locally on port 8080.
41 |
42 | ## Credits
43 |
44 | - The local web server is built using [swift-nio/http](https://github.com/apple/swift-nio).
45 | - HMAC and SHA1 hashing is performed using [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift).
46 | - RSA signing of service account JWT tokens uses [BigInt](https://github.com/attaswift/BigInt).
47 |
48 | ## Disclaimer
49 |
50 | This is work in progress toward great server-side Swift software. Please take care
51 | when using this in production projects: always refer to a tagged version and
52 | be aware that interfaces may change in future releases.
53 |
54 | ## Contributing
55 |
56 | We'd love to collaborate on this. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
57 |
58 | ## Copyright
59 |
60 | Copyright 2019, Google LLC.
61 |
62 | ## License
63 |
64 | Released under the Apache 2.0 license.
65 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
4 |
5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
6 |
7 | We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
8 |
--------------------------------------------------------------------------------
/Sources/Examples/GitHub/GitHub.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import Dispatch
17 | import OAuth2
18 |
19 | class GitHubSession {
20 |
21 | var connection : Connection
22 |
23 | init(tokenProvider: TokenProvider) {
24 | connection = Connection(provider:tokenProvider)
25 | }
26 |
27 | func getMe() throws {
28 | let sem = DispatchSemaphore(value: 0)
29 | var responseData : Data?
30 | try connection.performRequest(
31 | method:"GET",
32 | urlString:"https://api.github.com/user") {(data, response, error) in
33 | responseData = data
34 | sem.signal()
35 | }
36 | _ = sem.wait(timeout: DispatchTime.distantFuture)
37 | if let data = responseData {
38 | let response = String(data: data, encoding: .utf8)!
39 | print(response)
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/Examples/GitHub/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import OAuth2
17 |
18 | let CREDENTIALS = "github.json"
19 | let TOKEN = "github.json"
20 |
21 | func main() throws {
22 | let arguments = CommandLine.arguments
23 |
24 | if arguments.count == 1 {
25 | print("Usage: \(arguments[0]) [options]")
26 | return
27 | }
28 |
29 | guard let tokenProvider = BrowserTokenProvider(credentials:CREDENTIALS, token:TOKEN) else {
30 | print("Unable to create token provider.")
31 | return
32 | }
33 |
34 | let github = GitHubSession(tokenProvider:tokenProvider)
35 |
36 | if arguments[1] == "login" {
37 | try tokenProvider.signIn(scopes:["user"])
38 | try tokenProvider.saveToken(TOKEN)
39 | }
40 |
41 | if arguments[1] == "me" {
42 | try github.getMe()
43 | }
44 | }
45 |
46 | do {
47 | try main()
48 | } catch (let error) {
49 | print("ERROR: \(error)")
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/Sources/Examples/Google/Google.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import Dispatch
17 | import OAuth2
18 |
19 | class GoogleSession {
20 | var connection : Connection
21 |
22 | init(tokenProvider: TokenProvider) {
23 | connection = Connection(provider:tokenProvider)
24 | }
25 |
26 | func getMe() throws {
27 | let sem = DispatchSemaphore(value: 0)
28 |
29 | let parameters = ["requestMask.includeField": "person.names,person.photos"]
30 | var responseData : Data?
31 | try connection.performRequest(
32 | method:"GET",
33 | urlString:"https://people.googleapis.com/v1/people/me",
34 | parameters: parameters,
35 | body: nil) {(data, response, error) in
36 | responseData = data
37 | sem.signal()
38 | }
39 | _ = sem.wait(timeout: DispatchTime.distantFuture)
40 | if let data = responseData {
41 | let response = String(data: data, encoding: .utf8)!
42 | print(response)
43 | }
44 | }
45 |
46 | func getPeople() throws {
47 | let sem = DispatchSemaphore(value: 0)
48 | var responseData : Data?
49 | let parameters = ["requestMask.includeField": "person.names,person.photos"]
50 | try connection.performRequest(
51 | method:"GET",
52 | urlString:"https://people.googleapis.com/v1/people/me/connections",
53 | parameters: parameters,
54 | body:nil) {(data, response, error) in
55 | responseData = data
56 | sem.signal()
57 | }
58 | _ = sem.wait(timeout: DispatchTime.distantFuture)
59 | if let data = responseData {
60 | let response = String(data: data, encoding: .utf8)!
61 | print(response)
62 | }
63 | }
64 |
65 | func getData() throws {
66 | let sem = DispatchSemaphore(value: 0)
67 | var responseData : Data?
68 | let parameters : [String:String] = [:]
69 | let postJSON = ["gqlQuery":["queryString":"select *"]]
70 | let postData = try JSONSerialization.data(withJSONObject:postJSON)
71 | try connection.performRequest(
72 | method:"POST",
73 | urlString:"https://datastore.googleapis.com/v1/projects/hello-86:runQuery",
74 | parameters: parameters,
75 | body: postData) {(data, response, error) in
76 | responseData = data
77 | sem.signal()
78 | }
79 | _ = sem.wait(timeout: DispatchTime.distantFuture)
80 | if let data = responseData {
81 | let response = String(data: data, encoding: .utf8)!
82 | print(response)
83 | }
84 | //var request = Google_Datastore_V1_RunQueryRequest()
85 | //request.projectId = projectID
86 | //var query = Google_Datastore_V1_GqlQuery()
87 | //query.queryString = "select *"
88 | //request.gqlQuery = query
89 | //print("\(request)")
90 | //let result = try service.runquery(request)
91 | }
92 |
93 | func translate(_ input:String) throws {
94 | let sem = DispatchSemaphore(value: 0)
95 | var responseData : Data?
96 | let parameters : [String:String] = [:]
97 | let postJSON = ["q":input, "provider":"en", "target":"es", "format":"text"]
98 | let postData = try JSONSerialization.data(withJSONObject:postJSON)
99 | try connection.performRequest(
100 | method:"POST",
101 | urlString:"https://translation.googleapis.com/language/translate/v2",
102 | parameters: parameters,
103 | body: postData) {(data, response, error) in
104 | responseData = data
105 | sem.signal()
106 | }
107 | _ = sem.wait(timeout: DispatchTime.distantFuture)
108 | if let data = responseData {
109 | let response = String(data: data, encoding: .utf8)!
110 | print(response)
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Sources/Examples/Google/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import OAuth2
17 |
18 | let CREDENTIALS = "google.json"
19 | let TOKEN = "google.json"
20 |
21 | fileprivate enum CommandLineOption {
22 | case login
23 | case me
24 | case people
25 | case data
26 | case translate(text: String)
27 | init?(arguments: [String]) {
28 | if arguments.count > 1 {
29 | let command = arguments[1]
30 | switch command {
31 | case "login": self = .login
32 | case "me": self = .me
33 | case "people": self = .people
34 | case "data": self = .data
35 | case "translate":
36 | if arguments.count < 2 { return nil }
37 | self = .translate(text: arguments[2])
38 | default: return nil
39 | }
40 | } else {
41 | return nil
42 | }
43 | }
44 | func stringValue() -> String {
45 | switch self {
46 | case .login: return "login"
47 | case .me: return "me"
48 | case .people: return "people"
49 | case .data: return "data"
50 | case .translate(_): return "translate"
51 | }
52 | }
53 |
54 | static func all() -> [String] {
55 | return [CommandLineOption.login,
56 | CommandLineOption.me,
57 | CommandLineOption.people,
58 | CommandLineOption.data,
59 | CommandLineOption.translate(text: "")].map({$0.stringValue()})
60 | }
61 | }
62 |
63 | func main() throws {
64 |
65 | let arguments = CommandLine.arguments
66 | guard let option = CommandLineOption(arguments: arguments) else {
67 | print("Usage: \(arguments[0]) [options]")
68 | print("Options list: \(CommandLineOption.all())")
69 | return
70 | }
71 |
72 | let scopes = ["profile",
73 | "https://www.googleapis.com/auth/contacts.readonly",
74 | "https://www.googleapis.com/auth/cloud-platform"]
75 |
76 | guard let browserTokenProvider = BrowserTokenProvider(credentials:CREDENTIALS, token:TOKEN) else {
77 | print("Unable to create token provider.")
78 | return
79 | }
80 | let google = GoogleSession(tokenProvider:browserTokenProvider)
81 |
82 | switch option {
83 | case .login:
84 | try browserTokenProvider.signIn(scopes:scopes)
85 | try browserTokenProvider.saveToken(TOKEN)
86 | case .me:
87 | try google.getMe()
88 | case .people:
89 | try google.getPeople()
90 | case .data:
91 | try google.getData()
92 | case .translate(let text):
93 | try google.translate(text)
94 | }
95 |
96 | }
97 |
98 | do {
99 | try main()
100 | } catch (let error) {
101 | print("ERROR: \(error)")
102 | }
103 |
--------------------------------------------------------------------------------
/Sources/Examples/Meetup/Meetup.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import Dispatch
17 | import OAuth2
18 |
19 | class MeetupSession {
20 |
21 | var connection : Connection
22 |
23 | init(tokenProvider: TokenProvider) {
24 | connection = Connection(provider:tokenProvider)
25 | }
26 |
27 | func getMe() throws {
28 | let sem = DispatchSemaphore(value: 0)
29 | var responseData : Data?
30 | try connection.performRequest(
31 | method:"GET",
32 | urlString:"https://api.meetup.com/dashboard") {(data, response, error) in
33 | responseData = data
34 | sem.signal()
35 | }
36 | _ = sem.wait(timeout: DispatchTime.distantFuture)
37 | if let data = responseData {
38 | let response = String(data: data, encoding: .utf8)!
39 | print(response)
40 | }
41 | }
42 |
43 | func getRSVPs(eventid : String) throws {
44 | let sem = DispatchSemaphore(value: 0)
45 | var responseData : Data?
46 | try connection.performRequest(
47 | method:"GET",
48 | urlString:"https://api.meetup.com/sviphone/events/\(eventid)/rsvps") {(data, response, error) in
49 | responseData = data
50 | sem.signal()
51 | }
52 | _ = sem.wait(timeout: DispatchTime.distantFuture)
53 | if let data = responseData {
54 | let response = String(data: data, encoding: .utf8)!
55 | print(response)
56 | }
57 | }
58 |
59 | func getEvents() throws {
60 | let sem = DispatchSemaphore(value: 0)
61 | let parameters : [String:String] = ["status":"past"]
62 | var responseData : Data?
63 | try connection.performRequest(
64 | method:"GET",
65 | urlString:"https://api.meetup.com/sviphone/events",
66 | parameters: parameters,
67 | body: nil) {(data, response, error) in
68 | responseData = data
69 | sem.signal()
70 | }
71 | _ = sem.wait(timeout: DispatchTime.distantFuture)
72 | if let data = responseData {
73 | let response = String(data: data, encoding: .utf8)!
74 | print(response)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/Examples/Meetup/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import OAuth2
17 |
18 | let CREDENTIALS = "meetup.json"
19 | let TOKEN = "meetup.json"
20 |
21 | func main() throws {
22 | let arguments = CommandLine.arguments
23 |
24 | if arguments.count == 1 {
25 | print("Usage: \(arguments[0]) [options]")
26 | return
27 | }
28 |
29 | guard let tokenProvider = BrowserTokenProvider(credentials:CREDENTIALS, token:TOKEN) else {
30 | print("Unable to create token provider.")
31 | return
32 | }
33 |
34 | let meetup = MeetupSession(tokenProvider:tokenProvider)
35 |
36 | if arguments[1] == "login" {
37 | try tokenProvider.signIn(scopes:["basic", "ageless"])
38 | try tokenProvider.saveToken(TOKEN)
39 | }
40 |
41 | if arguments[1] == "me" {
42 | try meetup.getMe()
43 | }
44 |
45 | if arguments[1] == "rsvps" {
46 | try meetup.getRSVPs(eventid:arguments[2])
47 | }
48 |
49 | if arguments[1] == "events" {
50 | try meetup.getEvents()
51 | }
52 | }
53 |
54 | do {
55 | try main()
56 | } catch (let error) {
57 | print("ERROR: \(error)")
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/Sources/Examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | This directory contains examples that illustrate OAuth signin for
4 | various services. Each requires valid application credentials to run.
5 | See the various service providers for details and look in the
6 | [credentials](../../credentials) directory to see how to provide
7 | credentials to this library.
8 |
--------------------------------------------------------------------------------
/Sources/Examples/Spotify/Spotify.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import Dispatch
17 | import OAuth2
18 |
19 | class SpotifySession {
20 |
21 | var connection : Connection
22 |
23 | init(tokenProvider: TokenProvider) {
24 | connection = Connection(provider:tokenProvider)
25 | }
26 |
27 | func getUser() throws {
28 | let sem = DispatchSemaphore(value: 0)
29 | var responseData : Data?
30 | try connection.performRequest(
31 | method:"GET",
32 | urlString:"https://api.spotify.com/v1/me") {(data, response, error) in
33 | responseData = data
34 | sem.signal()
35 | }
36 | _ = sem.wait(timeout: DispatchTime.distantFuture)
37 | if let data = responseData {
38 | let response = String(data: data, encoding: .utf8)!
39 | print(response)
40 | }
41 | }
42 |
43 | func getTracks() throws {
44 | let sem = DispatchSemaphore(value: 0)
45 | var responseData : Data?
46 | try connection.performRequest(
47 | method:"GET",
48 | urlString:"https://api.spotify.com/v1/me/tracks") {(data, response, error) in
49 | responseData = data
50 | sem.signal()
51 | }
52 | _ = sem.wait(timeout: DispatchTime.distantFuture)
53 | if let data = responseData {
54 | let response = String(data: data, encoding: .utf8)!
55 | print(response)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/Examples/Spotify/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import OAuth2
17 |
18 | let CREDENTIALS = "spotify.json"
19 | let TOKEN = "spotify.json"
20 |
21 | func main() throws {
22 | let arguments = CommandLine.arguments
23 |
24 | if arguments.count == 1 {
25 | print("Usage: \(arguments[0]) [options]")
26 | return
27 | }
28 |
29 | guard let tokenProvider = BrowserTokenProvider(credentials:CREDENTIALS, token:TOKEN) else {
30 | print("Unable to create token provider.")
31 | return
32 | }
33 |
34 | let spotify = SpotifySession(tokenProvider:tokenProvider)
35 |
36 | if arguments[1] == "login" {
37 | try tokenProvider.signIn(scopes:["playlist-read-private",
38 | "playlist-modify-public",
39 | "playlist-modify-private",
40 | "user-library-read",
41 | "user-library-modify",
42 | "user-read-private",
43 | "user-read-email"])
44 | try tokenProvider.saveToken(TOKEN)
45 | }
46 |
47 | if arguments[1] == "me" {
48 | try spotify.getUser()
49 | }
50 |
51 | if arguments[1] == "tracks" {
52 | try spotify.getTracks()
53 | }
54 | }
55 |
56 | do {
57 | try main()
58 | } catch (let error) {
59 | print("ERROR: \(error)")
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/Sources/Examples/TokenSource/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import Dispatch
17 | import OAuth2
18 |
19 | let scopes = ["https://www.googleapis.com/auth/cloud-platform"]
20 |
21 | if let provider = DefaultTokenProvider(scopes: scopes) {
22 | let sem = DispatchSemaphore(value: 0)
23 | try provider.withToken() {(token, error) -> Void in
24 | if let token = token {
25 | let encoder = JSONEncoder()
26 | if let token = try? encoder.encode(token) {
27 | print("\(String(data:token, encoding:.utf8)!)")
28 | }
29 | }
30 | if let error = error {
31 | print("ERROR \(error)")
32 | }
33 | sem.signal()
34 | }
35 | _ = sem.wait(timeout: DispatchTime.distantFuture)
36 | } else {
37 | print("Unable to obtain an auth token.\nTry pointing GOOGLE_APPLICATION_CREDENTIALS to your service account credentials.")
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/Examples/Twitter/Twitter.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import Dispatch
17 | import OAuth1
18 |
19 | class TwitterSession {
20 | public var connection : Connection
21 |
22 | init(tokenProvider: TokenProvider) {
23 | connection = Connection(provider:tokenProvider)
24 | }
25 |
26 | func getTweets() throws {
27 | let sem = DispatchSemaphore(value: 0)
28 | var responseData : Data?
29 | try connection.performRequest(
30 | method:"GET",
31 | urlString:"https://api.twitter.com/1.1/statuses/user_timeline.json") {(data, response, error) in
32 | responseData = data
33 | sem.signal()
34 | }
35 | _ = sem.wait(timeout: DispatchTime.distantFuture)
36 | if let data = responseData {
37 | let response = String(data: data, encoding: .utf8)!
38 | print(response)
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Examples/Twitter/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import OAuth1
17 |
18 | let CREDENTIALS = "twitter.json"
19 | let TOKEN = "twitter.json"
20 |
21 | func main() throws {
22 | let arguments = CommandLine.arguments
23 |
24 | if arguments.count == 1 {
25 | print("Usage: \(arguments[0]) [options]")
26 | return
27 | }
28 |
29 | guard let tokenProvider = BrowserTokenProvider(credentials:CREDENTIALS, token:TOKEN) else {
30 | print("Unable to create token provider.")
31 | return
32 | }
33 |
34 | let twitter = TwitterSession(tokenProvider:tokenProvider)
35 |
36 | if arguments[1] == "login" {
37 | try tokenProvider.signIn()
38 | try tokenProvider.saveToken(TOKEN)
39 | }
40 |
41 | if arguments[1] == "tweets" {
42 | try twitter.getTweets()
43 | }
44 | }
45 |
46 | do {
47 | try main()
48 | } catch (let error) {
49 | print("ERROR: \(error)")
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/Sources/OAuth1/BrowserTokenProvider.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | #if canImport(FoundationNetworking)
17 | import FoundationNetworking
18 | #endif
19 | import CryptoSwift
20 | import Dispatch
21 | import NIOHTTP1
22 | import TinyHTTPServer
23 |
24 | struct Credentials: Codable {
25 | let consumerKey: String
26 | let consumerSecret: String
27 | let requestTokenURL: String
28 | let authorizeURL: String
29 | let accessTokenURL: String
30 | let callback: String
31 | enum CodingKeys: String, CodingKey {
32 | case consumerKey = "consumer_key"
33 | case consumerSecret = "consumer_secret"
34 | case requestTokenURL = "request_token_url"
35 | case authorizeURL = "authorize_url"
36 | case accessTokenURL = "access_token_url"
37 | case callback
38 | }
39 | }
40 |
41 | enum AuthError: Error {
42 | case invalidTokenFile
43 | case tokenRequestFailed
44 | }
45 |
46 | public class BrowserTokenProvider: TokenProvider {
47 | private var credentials: Credentials
48 | public var token: Token?
49 |
50 | private var sem: DispatchSemaphore?
51 |
52 | public init?(credentials: String, token tokenfile: String) {
53 | let path = ProcessInfo.processInfo.environment["HOME"]!
54 | + "/.credentials/" + credentials
55 | let url = URL(fileURLWithPath: path)
56 |
57 | guard let credentialsData = try? Data(contentsOf: url) else {
58 | print("No credentials data at \(path).")
59 | return nil
60 | }
61 | let decoder = JSONDecoder()
62 | guard let credentials = try? decoder.decode(Credentials.self,
63 | from: credentialsData)
64 | else {
65 | print("Error reading credentials")
66 | return nil
67 | }
68 | self.credentials = credentials
69 |
70 | if tokenfile != "" {
71 | do {
72 | let data = try Data(contentsOf: URL(fileURLWithPath: tokenfile))
73 | let decoder = JSONDecoder()
74 | guard let token = try? decoder.decode(Token.self, from: data)
75 | else {
76 | return nil
77 | }
78 | self.token = token
79 | } catch {
80 | // ignore errors due to missing session files
81 | }
82 | }
83 | }
84 |
85 | public func saveToken(_ filename: String) throws {
86 | if let token = token {
87 | try token.save(filename)
88 | }
89 | }
90 |
91 | // StartServer starts a web server that listens on http://localhost:8080.
92 | // The webserver waits for an oauth code in the three-legged auth flow.
93 | private func startServer(sem: DispatchSemaphore) throws {
94 | self.sem = sem
95 |
96 | try TinyHTTPServer().start { server, request -> (String, HTTPResponseStatus) in
97 | if request.uri.unicodeScalars.starts(with: self.credentials.callback.unicodeScalars) {
98 | server.stop()
99 | if let urlComponents = URLComponents(string: request.uri) {
100 | self.token = Token(urlComponents: urlComponents)
101 | DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
102 | self.sem?.signal()
103 | }
104 | return ("success! Token received.\n", .ok)
105 | } else {
106 | return ("failed to get token.\n", .ok)
107 | }
108 | } else {
109 | return ("not found\n", .notFound)
110 | }
111 | }
112 | }
113 |
114 | @available(iOS 10.0, tvOS 10.0, *)
115 | public func signIn() throws {
116 | let sem = DispatchSemaphore(value: 0)
117 | try startServer(sem: sem)
118 |
119 | let sem2 = DispatchSemaphore(value: 0)
120 |
121 | let parameters = ["oauth_callback": "http://localhost:8080" + credentials.callback]
122 |
123 | var data: Data?
124 | var response: HTTPURLResponse?
125 | var error: Error?
126 |
127 | Connection.performRequest(
128 | method: "POST",
129 | urlString: credentials.requestTokenURL,
130 | parameters: parameters,
131 | tokenSecret: "",
132 | consumerKey: credentials.consumerKey,
133 | consumerSecret: credentials.consumerSecret
134 | ) { d, r, e in
135 | data = d
136 | response = r as! HTTPURLResponse?
137 | error = e
138 | sem2.signal()
139 | }
140 | _ = sem2.wait(timeout: DispatchTime.distantFuture)
141 |
142 | if let error = error {
143 | throw error
144 | }
145 |
146 | if let response = response,
147 | let data = data {
148 | if response.statusCode != 200 {
149 | throw AuthError.tokenRequestFailed
150 | }
151 | guard let params = String(data: data, encoding: .utf8) else {
152 | throw AuthError.tokenRequestFailed
153 | }
154 |
155 | token = Token(urlComponents: URLComponents(string: "http://example.com?" + params)!)
156 |
157 | var urlComponents = URLComponents(string: credentials.authorizeURL)!
158 | urlComponents.queryItems = [URLQueryItem(name: "oauth_token", value: encode(token!.oAuthToken!))]
159 | openURL(urlComponents.url!)
160 | _ = sem.wait(timeout: DispatchTime.distantFuture)
161 | try exchange()
162 | } else {
163 | throw AuthError.tokenRequestFailed
164 | }
165 | }
166 |
167 | private func exchange() throws {
168 | let sem = DispatchSemaphore(value: 0)
169 | let parameters = [
170 | "oauth_token": token!.oAuthToken!,
171 | "oauth_verifier": token!.oAuthVerifier!,
172 | ]
173 | var responseData: Data?
174 | Connection.performRequest(
175 | method: "POST",
176 | urlString: credentials.accessTokenURL,
177 | parameters: parameters,
178 | tokenSecret: "",
179 | consumerKey: credentials.consumerKey,
180 | consumerSecret: credentials.consumerSecret
181 | ) { data, _, _ in
182 | responseData = data
183 | sem.signal()
184 | }
185 | _ = sem.wait(timeout: DispatchTime.distantFuture)
186 | let urlComponents = URLComponents(string: "http://example.com?" + String(data: responseData!, encoding: .utf8)!)!
187 | token = Token(urlComponents: urlComponents)
188 | }
189 |
190 | public func withToken(_ callback: @escaping (Token?, String?, String?, Error?) -> Void) throws {
191 | callback(token, credentials.consumerKey, credentials.consumerSecret, nil)
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/Sources/OAuth1/Connection.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | #if canImport(FoundationNetworking)
17 | import FoundationNetworking
18 | #endif
19 | import Dispatch
20 | import CryptoSwift
21 |
22 | public class Connection {
23 |
24 | public var provider : TokenProvider
25 |
26 | public init(provider : TokenProvider) {
27 | self.provider = provider
28 | }
29 |
30 | class func signOAuthRequest(
31 | method : String,
32 | urlString : String,
33 | parameters : inout [String:String],
34 | secret : String) {
35 |
36 | // sort the keys of the method call
37 | let sortedMethodKeys = Array(parameters.keys).sorted(by:<)
38 |
39 | // build the signature base string
40 | var presignature = ""
41 |
42 | for key in sortedMethodKeys {
43 | if presignature != "" {
44 | presignature += "&"
45 | }
46 | presignature += key + "=" + encode(parameters[key]!)
47 | }
48 | let signatureBaseString = method + "&" + encode(urlString) + "&" + encode(presignature)
49 |
50 | // generate the signature
51 | let hmac = try! CryptoSwift.HMAC(key: secret, variant: .sha1).authenticate(Array(signatureBaseString.utf8))
52 | parameters["oauth_signature"] = hmac.toBase64()!
53 | }
54 |
55 | public class func performRequest(
56 | method : String,
57 | urlString : String,
58 | parameters : [String:String],
59 | tokenSecret : String,
60 | consumerKey : String,
61 | consumerSecret : String,
62 | callback: @escaping (Data?, URLResponse?, Error?)->()) {
63 |
64 | // prepare the request for signing
65 | var parameters = parameters
66 | parameters["oauth_consumer_key"] = consumerKey
67 | parameters["oauth_version"] = "1.0"
68 | parameters["oauth_nonce"] = UUID().uuidString
69 | parameters["oauth_timestamp"] = String(Int(NSDate().timeIntervalSince1970))
70 | parameters["oauth_signature_method"] = "HMAC-SHA1"
71 |
72 | // sign the request
73 | signOAuthRequest(method:method, urlString:urlString, parameters:¶meters, secret:consumerSecret + "&" + tokenSecret)
74 |
75 | // sort the keys of the method call
76 | let sortedMethodKeys = Array(parameters.keys).sorted(by:<)
77 |
78 | // build the authorization string
79 | var authorization = "OAuth "
80 | var i = 0
81 | for key in sortedMethodKeys {
82 | if key.hasPrefix("oauth_") {
83 | if i > 0 {
84 | authorization += ", "
85 | }
86 | authorization += key + "=\"" + encode(parameters[key]!) + "\""
87 | i += 1
88 | }
89 | }
90 |
91 | var urlComponents = URLComponents(string:urlString)!
92 | var queryItems : [URLQueryItem] = []
93 | for key in sortedMethodKeys {
94 | if !key.hasPrefix("oauth_") {
95 | queryItems.append(URLQueryItem(name: key, value: parameters[key]))
96 | }
97 | }
98 | urlComponents.queryItems = queryItems
99 |
100 | var request = URLRequest(url: urlComponents.url!)
101 | request.setValue(authorization, forHTTPHeaderField:"Authorization")
102 | request.httpMethod = method
103 |
104 | let session = URLSession(configuration: URLSessionConfiguration.default)
105 | let task: URLSessionDataTask = session.dataTask(with:request) { (data, response, error) -> Void in
106 | callback(data, response, error)
107 | }
108 | task.resume()
109 | }
110 |
111 | public func performRequest(
112 | method : String,
113 | urlString : String,
114 | parameters : [String:String],
115 | callback: @escaping (Data?, URLResponse?, Error?)->()) throws {
116 |
117 | try provider.withToken() {(token, consumerKey, consumerSecret, err) in
118 | guard let token = token else {
119 | return
120 | }
121 | guard let oAuthToken = token.oAuthToken,
122 | let oAuthTokenSecret = token.oAuthTokenSecret else {
123 | return
124 | }
125 | var parameters = parameters
126 | parameters["oauth_token"] = oAuthToken
127 | Connection.performRequest(
128 | method: method,
129 | urlString: urlString,
130 | parameters: parameters,
131 | tokenSecret: oAuthTokenSecret,
132 | consumerKey: consumerKey!,
133 | consumerSecret: consumerSecret!,
134 | callback: callback)
135 | }
136 | }
137 |
138 | public func performRequest(
139 | method : String,
140 | urlString : String,
141 | callback: @escaping (Data?, URLResponse?, Error?)->()) throws {
142 | let parameters : [String:String] = [:]
143 | try self.performRequest(method:method, urlString:urlString, parameters:parameters, callback:callback)
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/Sources/OAuth1/Token.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | import Foundation
15 |
16 | public struct Token : Codable {
17 | public var oAuthToken: String?
18 | public var oAuthTokenSecret: String?
19 | public var oAuthVerifier: String?
20 | public var returnAddress: String?
21 | public var screenName: String?
22 | public var userID: String?
23 | public var creationTime: Date?
24 |
25 | enum CodingKeys: String, CodingKey {
26 | case oAuthToken = "oauth_token"
27 | case oAuthTokenSecret = "oauth_token_secret"
28 | case oAuthVerifier = "oauth_verifier"
29 | case returnAddress = "return_address"
30 | case screenName = "screen_name"
31 | case userID = "user_id"
32 | case creationTime = "creation_time"
33 | }
34 |
35 | public init(urlComponents: URLComponents) {
36 | creationTime = Date()
37 | for queryItem in urlComponents.queryItems! {
38 | if let value = queryItem.value {
39 | switch queryItem.name {
40 | case "oauth_token":
41 | oAuthToken = value
42 | case "oauth_token_secret":
43 | oAuthTokenSecret = value
44 | case "oauth_verifier":
45 | oAuthVerifier = value
46 | case "screen_name":
47 | screenName = value
48 | case "user_id":
49 | userID = value
50 | case "oauth_callback_confirmed":
51 | break
52 | default:
53 | break
54 | }
55 | }
56 | }
57 | }
58 |
59 | func save(_ filename: String) throws {
60 | let encoder = JSONEncoder()
61 | let data = try encoder.encode(self)
62 | try data.write(to: URL(fileURLWithPath: filename))
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/OAuth1/TokenProvider.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | public protocol TokenProvider {
16 | func withToken(_ callback:@escaping (Token?, String?, String?, Error?) -> Void) throws
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/OAuth1/encode.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | import Foundation
15 |
16 | internal func encode(_ s: String) -> String {
17 | let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~")
18 | return s.addingPercentEncoding(withAllowedCharacters: allowed)!
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/OAuth1/openURL.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | #if os(OSX)
17 | import Cocoa
18 | #elseif os(iOS) || os(tvOS)
19 | import UIKit
20 | #endif
21 |
22 | #if !(os(iOS) || os(tvOS))
23 | private func shell(_ args: String...) -> Int32 {
24 | let task = Process()
25 | if #available(macOS 10.13, *) {
26 | task.executableURL = URL(string: "/usr/bin/env")
27 | } else {
28 | task.launchPath = "/usr/bin/env"
29 | }
30 | task.arguments = args
31 | if #available(macOS 10.13, *) {
32 | do {
33 | try task.run()
34 | } catch {
35 | return -1
36 | }
37 | } else {
38 | task.launch()
39 | }
40 | task.waitUntilExit()
41 | return task.terminationStatus
42 | }
43 | #endif
44 |
45 | @available(iOS 10.0, tvOS 10.0, *)
46 | internal func openURL(_ url: URL) {
47 | #if os(OSX)
48 | if !NSWorkspace.shared.open(url) {
49 | print("default browser could not be opened")
50 | }
51 | #elseif os(iOS) || os(tvOS)
52 | UIApplication.shared.open(url) { success in
53 | if !success {
54 | print("default browser could not be opened")
55 | }
56 | }
57 | #else // Linux, tested on Ubuntu
58 | let status = shell("xdg-open", String(describing:url))
59 | if status != 0 {
60 | print("To continue, please open this URL in your browser: (\(String(describing:url)))")
61 | }
62 | #endif
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/OAuth2/AuthError.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | import Foundation
15 |
16 | public enum AuthError: Error {
17 | case noRefreshToken
18 | case unknownError
19 | case webSession(inner: Error)
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/OAuth2/BrowserTokenProvider.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Dispatch
16 | import Foundation
17 | #if canImport(FoundationNetworking)
18 | import FoundationNetworking
19 | #endif
20 | import NIOHTTP1
21 | import TinyHTTPServer
22 |
23 | struct Credentials: Codable, CodeExchangeInfo, RefreshExchangeInfo {
24 | let clientID: String
25 | let clientSecret: String
26 | let authorizeURL: String
27 | let accessTokenURL: String
28 | let callback: String
29 | enum CodingKeys: String, CodingKey {
30 | case clientID = "client_id"
31 | case clientSecret = "client_secret"
32 | case authorizeURL = "authorize_url"
33 | case accessTokenURL = "access_token_url"
34 | case callback
35 | }
36 | var redirectURI: String {
37 | "http://localhost:8080" + self.callback
38 | }
39 | }
40 |
41 | public class BrowserTokenProvider: TokenProvider {
42 | private var credentials: Credentials
43 | private var code: Code?
44 | public var token: Token?
45 |
46 | private var sem: DispatchSemaphore?
47 |
48 | public convenience init?(credentials: String, token tokenfile: String) {
49 | let path = ProcessInfo.processInfo.environment["HOME"]!
50 | + "/.credentials/" + credentials
51 | let url = URL(fileURLWithPath: path)
52 |
53 | guard let credentialsData = try? Data(contentsOf: url) else {
54 | print("No credentials data at \(path).")
55 | return nil
56 | }
57 | self.init(credentials: credentialsData, token: tokenfile)
58 | }
59 |
60 | public init?(credentials: Data, token tokenfile: String) {
61 | let decoder = JSONDecoder()
62 | guard let credentials = try? decoder.decode(Credentials.self,
63 | from: credentials)
64 | else {
65 | print("Error reading credentials")
66 | return nil
67 | }
68 | self.credentials = credentials
69 |
70 | if tokenfile != "" {
71 | do {
72 | let data = try Data(contentsOf: URL(fileURLWithPath: tokenfile))
73 | let decoder = JSONDecoder()
74 | guard let token = try? decoder.decode(Token.self, from: data)
75 | else {
76 | return nil
77 | }
78 | self.token = token
79 | } catch {
80 | // ignore errors due to missing token files
81 | }
82 | }
83 | }
84 |
85 | public func saveToken(_ filename: String) throws {
86 | if let token = token {
87 | try token.save(filename)
88 | }
89 | }
90 |
91 | public func refreshToken(_ filename: String) throws {
92 | if let token = token, token.isExpired() {
93 | self.token = try Refresh(token: token).exchange(info: credentials)
94 | try saveToken(filename)
95 | }
96 | }
97 |
98 | // StartServer starts a web server that listens on http://localhost:8080.
99 | // The webserver waits for an oauth code in the three-legged auth flow.
100 | private func startServer(sem: DispatchSemaphore) throws {
101 | self.sem = sem
102 |
103 | try TinyHTTPServer().start { server, request -> (String, HTTPResponseStatus) in
104 | if request.uri.unicodeScalars.starts(with: self.credentials.callback.unicodeScalars) {
105 | server.stop()
106 | if let urlComponents = URLComponents(string: request.uri) {
107 | self.code = Code(urlComponents: urlComponents)
108 | DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
109 | self.sem?.signal()
110 | }
111 | return ("success! Token received.\n", .ok)
112 | } else {
113 | return ("failed to get token.\n", .ok)
114 | }
115 | } else {
116 | return ("not found\n", .notFound)
117 | }
118 | }
119 | }
120 |
121 | @available(iOS 10.0, tvOS 10.0, *)
122 | public func signIn(scopes: [String]) throws {
123 | let sem = DispatchSemaphore(value: 0)
124 | try startServer(sem: sem)
125 |
126 | let state = UUID().uuidString
127 | let scope = scopes.joined(separator: " ")
128 |
129 | var urlComponents = URLComponents(string: credentials.authorizeURL)!
130 | urlComponents.queryItems = [
131 | URLQueryItem(name: "client_id", value: credentials.clientID),
132 | URLQueryItem(name: "response_type", value: "code"),
133 | URLQueryItem(name: "redirect_uri", value: credentials.redirectURI),
134 | URLQueryItem(name: "state", value: state),
135 | URLQueryItem(name: "scope", value: scope),
136 | URLQueryItem(name: "show_dialog", value: "false"),
137 | ]
138 | openURL(urlComponents.url!)
139 | _ = sem.wait(timeout: DispatchTime.distantFuture)
140 | token = try code!.exchange(info: credentials)
141 | }
142 |
143 | public func withToken(_ callback: @escaping (Token?, Error?) -> Void) throws {
144 | callback(token, nil)
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/Sources/OAuth2/Code.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Dispatch
16 | import Foundation
17 | #if canImport(FoundationNetworking)
18 | import FoundationNetworking
19 | #endif
20 | import NIOHTTP1
21 | import TinyHTTPServer
22 |
23 | // For the browser and native flows, an oauth2 authorization code must be
24 | // exchanged for a live token by the user's agent/client.
25 |
26 | protocol CodeExchangeInfo {
27 | var accessTokenURL: String { get }
28 | var clientID: String { get }
29 | var clientSecret: String { get }
30 | var redirectURI: String { get }
31 | }
32 |
33 | class Code {
34 | var code: String?
35 | var state: String?
36 | var error: String?
37 |
38 | init(urlComponents: URLComponents) {
39 | for queryItem in urlComponents.queryItems! {
40 | if let value = queryItem.value {
41 | switch queryItem.name {
42 | case "code":
43 | code = value
44 | case "state":
45 | state = value
46 | case "error":
47 | error = value
48 | default:
49 | break
50 | }
51 | }
52 | }
53 | }
54 |
55 | func exchange(info: CodeExchangeInfo) throws -> Token {
56 | let sem = DispatchSemaphore(value: 0)
57 | let parameters = [
58 | "client_id": info.clientID, // some providers require the client id and secret in the method call
59 | "client_secret": info.clientSecret,
60 | "grant_type": "authorization_code",
61 | "code": self.code!,
62 | "redirect_uri": info.redirectURI,
63 | ]
64 | let token = info.clientID + ":" + info.clientSecret
65 | // some providers require the client id and secret in the authorization header
66 | let authorization = "Basic " + String(data: token.data(using: .utf8)!.base64EncodedData(), encoding: .utf8)!
67 | var responseData: Data?
68 | var contentType: String?
69 | Connection.performRequest(
70 | method: "POST",
71 | urlString: info.accessTokenURL,
72 | parameters: parameters,
73 | body: nil,
74 | authorization: authorization
75 | ) { data, response, _ in
76 | if let response = response as? HTTPURLResponse {
77 | for (k, v) in response.allHeaderFields {
78 | // Explicitly-lowercasing seems like it should be unnecessary,
79 | // but some services returned "Content-Type" and others sent "content-type".
80 | if (k as! String).lowercased() == "content-type" {
81 | contentType = v as? String
82 | }
83 | }
84 | }
85 | responseData = data
86 | sem.signal()
87 | }
88 | _ = sem.wait(timeout: DispatchTime.distantFuture)
89 | if let contentType = contentType, contentType.contains("application/json") {
90 | return try JSONDecoder().decode(Token.self, from: responseData!)
91 | } else { // assume "application/x-www-form-urlencoded"
92 | guard let responseData = responseData else {
93 | throw AuthError.unknownError
94 | }
95 | guard let queryParameters = String(data: responseData, encoding: .utf8) else {
96 | throw AuthError.unknownError
97 | }
98 | guard let urlComponents = URLComponents(string: "http://example.com?" + queryParameters) else {
99 | throw AuthError.unknownError
100 | }
101 | return Token(urlComponents: urlComponents)
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Sources/OAuth2/Connection.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | #if canImport(FoundationNetworking)
17 | import FoundationNetworking
18 | #endif
19 | import Dispatch
20 | import CryptoSwift
21 |
22 | public class Connection {
23 | public var provider: TokenProvider
24 |
25 | public init(provider: TokenProvider) {
26 | self.provider = provider
27 | }
28 |
29 | public class func performRequest(
30 | method: String,
31 | urlString: String,
32 | parameters: [String: String],
33 | body: Data!,
34 | authorization: String,
35 | callback: @escaping (Data?, URLResponse?, Error?) -> Void) {
36 |
37 | var urlComponents = URLComponents(string: urlString)!
38 |
39 | var queryItems: [URLQueryItem] = urlComponents.queryItems ?? []
40 | for (key, value) in parameters {
41 | queryItems.append(URLQueryItem(name: key, value: value))
42 | }
43 | if method == "GET" || body != nil {
44 | urlComponents.queryItems = queryItems
45 | }
46 |
47 | var request = URLRequest(url: urlComponents.url!)
48 | request.setValue(authorization, forHTTPHeaderField: "Authorization")
49 | request.httpMethod = method
50 | if method == "POST" || method == "PUT" {
51 | if let body = body {
52 | request.httpBody = body
53 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
54 | } else {
55 | var postComponents = URLComponents(string: "")!
56 | postComponents.queryItems = queryItems
57 | let string = postComponents.url!.query!
58 | request.httpBody = string.data(using: .utf8)
59 | }
60 | }
61 |
62 | let session = URLSession(configuration: URLSessionConfiguration.default)
63 | let task: URLSessionDataTask = session.dataTask(with: request) { (data, response, error) -> Void in
64 | callback(data, response, error)
65 | }
66 | task.resume()
67 | }
68 |
69 | public func performRequest(
70 | method: String,
71 | urlString: String,
72 | parameters: [String: String],
73 | body: Data!,
74 | callback: @escaping (Data?, URLResponse?, Error?) -> Void) throws {
75 |
76 | try provider.withToken {token, err in
77 | guard let accessToken = token?.AccessToken else {
78 | return callback(nil, nil, AuthError.unknownError)
79 | }
80 | Connection.performRequest(
81 | method: method,
82 | urlString: urlString,
83 | parameters: parameters,
84 | body: body,
85 | authorization: "Bearer " + accessToken,
86 | callback: callback)
87 | }
88 | }
89 |
90 | public func performRequest(
91 | method: String,
92 | urlString: String,
93 | callback: @escaping (Data?, URLResponse?, Error?) -> Void) throws {
94 |
95 | let parameters: [String: String] = [:]
96 | try performRequest(
97 | method: method,
98 | urlString: urlString,
99 | parameters: parameters,
100 | body: nil,
101 | callback: callback)
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Sources/OAuth2/DefaultTokenProvider.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 |
17 | // This class implements Google Application Default Credentials.
18 | // https://developers.google.com/identity/protocols/application-default-credentials
19 |
20 | public class DefaultTokenProvider : TokenProvider {
21 | public var token: Token?
22 | private var tokenProvider : TokenProvider
23 |
24 | public init?(scopes:[String]) {
25 | // if GOOGLE_APPLICATION_CREDENTIALS is set,
26 | // use it to get service account credentials.
27 | if let credentialsPath = ProcessInfo.processInfo.environment["GOOGLE_APPLICATION_CREDENTIALS"] {
28 | let credentialsURL = URL(fileURLWithPath:credentialsPath)
29 | guard let provider = ServiceAccountTokenProvider(credentialsURL:credentialsURL, scopes:scopes) else {
30 | return nil
31 | }
32 | tokenProvider = provider
33 | return
34 | }
35 | // if $HOME/.config/gcloud/application_default_credentials.json exists,
36 | // use it to request an access token and warn the user about possible
37 | // problems.
38 | if let home = ProcessInfo.processInfo.environment["HOME"] {
39 | let credentialsPath = home + "/.config/gcloud/application_default_credentials.json"
40 | if FileManager.default.fileExists(atPath:credentialsPath) {
41 | let credentialsURL = URL(fileURLWithPath:credentialsPath)
42 | guard let provider = GoogleRefreshTokenProvider(credentialsURL:credentialsURL) else {
43 | return nil
44 | }
45 | tokenProvider = provider
46 | print("""
47 | Your application has authenticated using end user credentials from Google
48 | Cloud SDK. We recommend that most server applications use service accounts
49 | instead. If your application continues to use end user credentials from Cloud
50 | SDK, you might receive a "quota exceeded" or "API not enabled" error. For
51 | more information about service accounts, see
52 | https://cloud.google.com/docs/authentication/.
53 | """)
54 | return
55 | }
56 | }
57 | // as a last resort, assume we are running on Google Compute Engine or Google App Engine
58 | // and try to get a token from the metadata service.
59 | guard let provider = GoogleCloudMetadataTokenProvider() else {
60 | return nil
61 | }
62 | tokenProvider = provider
63 | }
64 |
65 | public func withToken(_ callback:@escaping (Token?, Error?) -> Void) throws {
66 | try tokenProvider.withToken() {(token, error) in
67 | self.token = token
68 | callback(token, error)
69 | }
70 | }
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/Sources/OAuth2/FCMTokenProvider/FCMTokenProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | //
3 | // Copyright 2019 Google LLC. All Rights Reserved.
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 | //
17 |
18 | import Foundation
19 | import FirebaseFunctions
20 | import FirebaseCore
21 | import FirebaseAuth
22 |
23 | struct TokenServiceConstants {
24 | static let token = "Token"
25 | static let accessToken = "accessToken"
26 | static let expireTime = "expireTime"
27 | static let tokenReceived = "tokenReceived"
28 | static let retreivingToken = "RetrievingToken"
29 | static let getTokenAPI = "getOAuthToken"
30 | static let tokenType = "Bearer "
31 | static let noTokenError = "No token is available"
32 | }
33 |
34 |
35 | public class FCMTokenProvider {
36 |
37 | static private func retrieveAccessToken(deviceID: String, completionHandler: @escaping (Error?) -> Void) {
38 | Functions.functions().httpsCallable(TokenServiceConstants.getTokenAPI).call(["deviceID": deviceID], completion: { (result, error) in
39 | if error != nil {
40 | completionHandler(error)
41 | return
42 | }
43 | guard let _: HTTPSCallableResult = result else {
44 | completionHandler("Result found nil" as? Error)
45 | return
46 | }
47 | completionHandler(nil)
48 | })
49 | }
50 |
51 | //This function compares token expiry date with current date
52 | //Returns bool value True if the token is expired else false
53 | static private func isExpired() -> Bool {
54 | guard let token = UserDefaults.standard.value(forKey: TokenServiceConstants.token) as? [String: String],
55 | let expDate = token[TokenServiceConstants.expireTime] else{
56 | return true
57 | }
58 | let dateFormatter = DateFormatter()
59 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
60 | guard let expiryDate = dateFormatter.date(from: expDate) else {
61 | return true
62 | }
63 | return (Date() > expiryDate)
64 | }
65 |
66 | //Return token from user defaults if token is there and not expired.
67 | //Request for new token if token is expired or not there in user defaults.
68 | //Return the newly generated token.
69 | static public func getToken(deviceID: String, _ callback: @escaping (_ shouldWait:Bool, _ token: String?, _ error: Error?) -> Void) {
70 | if isExpired() {
71 | NotificationCenter.default.post(name: NSNotification.Name(TokenServiceConstants.retreivingToken), object: nil)
72 | //NotificationCenter.default.addObserver(self, selector: #selector(tokenReceived(tokenData:)), name: NSNotification.Name(TokenServiceConstants.tokenReceived), object: nil)
73 | //this sample uses Firebase Auth signInAnonymously and you can insert any auth signin that they offer.
74 | //FirebaseApp.configure()
75 | Auth.auth().signInAnonymously() { (authResult, error) in
76 | if error != nil {
77 | //Sign in failed
78 | callback(false, nil, error)
79 | return
80 | }
81 | retrieveAccessToken(deviceID: deviceID, completionHandler: {(error) in
82 | if let error = error {
83 | callback(false, nil, error)
84 | } else {
85 | callback(true, nil, nil)
86 | }
87 | })
88 | }
89 | } else {
90 | if let tokenData = UserDefaults.standard.value(forKey: TokenServiceConstants.token) as? [String: Any],
91 | let accessToken = tokenData[TokenServiceConstants.accessToken] as? String {
92 | let tokenModel = "\(TokenServiceConstants.tokenType)\(accessToken)"
93 | callback(false, tokenModel, nil)
94 | } else {
95 | UserDefaults.standard.set(nil, forKey: TokenServiceConstants.token)
96 | getToken(deviceID: deviceID, callback)
97 | }
98 | }
99 | }
100 |
101 | static public func tokenFromAppDelegate(tokenDict: [String: Any]) {
102 | UserDefaults.standard.set(tokenDict, forKey: TokenServiceConstants.token)
103 | }
104 |
105 | static public func getTokenFromUserDefaults() -> String {
106 | guard let tokenData = UserDefaults.standard.value(forKey: TokenServiceConstants.token) as? [String: String],
107 | let token = tokenData[TokenServiceConstants.accessToken] else{
108 | return "Token is not there in user defaults"
109 | }
110 | return TokenServiceConstants.tokenType + token
111 | }
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/Sources/OAuth2/FCMTokenProvider/README.md:
--------------------------------------------------------------------------------
1 | # Use Firebase Functions to Securely Obtain Auth Tokens
2 |
3 | The FirebaseFunctionTokenProvider allows applications to obtain auth tokens
4 | using service accounts without storing the service account credentials locally
5 | within an application. Instead, an app is registered to use Firebase Functions
6 | and calls a designated function that uses server-side calls to get an
7 | authorization token from a Firebase Function.
8 |
9 | A sample Firebase Function implementation is in `index.js`. This sample uses
10 | the Google Cloud Metadata Service to return an auth token associated with the
11 | service account of the Firebase Function. It triggers push notification with
12 | payload (contains token and it's expiry time) along with Device ID via
13 | FCM(Firebase Cloud Messaging) which will be received by client.
14 |
15 |
--------------------------------------------------------------------------------
/Sources/OAuth2/FCMTokenProvider/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | /*jshint esversion: 6 */
17 | /* jshint node: true */
18 |
19 | 'use strict';
20 |
21 | const admin = require('firebase-admin');
22 | const functions = require('firebase-functions');
23 | const http = require('http');
24 | const https = require('https');
25 | admin.initializeApp(functions.config().firebase);
26 | const db = admin.firestore();
27 |
28 | // [START save_token_to_firebase]
29 | function saveOAuthToken(context, oauthToken) {
30 | const docRef = db.collection('ShortLivedAuthTokens').doc('OauthToken');
31 | docRef.set(oauthToken);
32 | }
33 | // [END save_token_to_firebase]
34 |
35 | // [START generate_token]
36 | function generateAccessToken(
37 | context,
38 | serviceAccountAccessToken,
39 | serviceAccountTokenType
40 | ) {
41 | // With the service account's credentials, we can make a request to generate
42 | // a new token for a 2nd service account that only has the permission to
43 | // act as a Dialogflow Client
44 | return new Promise(resolve => {
45 | const post_options = {
46 | host: 'iamcredentials.googleapis.com',
47 | path:
48 | '/v1/projects/-/serviceAccounts/SERVICE-ACCOUNT-NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com:generateAccessToken',
49 | method: 'POST',
50 | headers: {
51 | // Set Service Account Credentials
52 | Authorization:
53 | serviceAccountTokenType + ' ' + serviceAccountAccessToken,
54 | },
55 | };
56 |
57 | // Set up the request
58 | let oauthToken = '';
59 | const post_req = https.request(post_options, res => {
60 | res.setEncoding('utf8');
61 | res.on('data', chunk => {
62 | oauthToken += chunk;
63 | });
64 | res.on('end', () => {
65 | // Next step in pipeline
66 | saveOAuthToken(context, JSON.parse(oauthToken));
67 | return resolve(JSON.parse(oauthToken));
68 | });
69 | });
70 |
71 | post_req.on('error', e => {
72 | console.log('ERROR generating new token', e.message);
73 | return 'Error retrieving token';
74 | });
75 |
76 | // Sets up the scope that we want the end user to have.
77 | const body = {
78 | delegates: [],
79 | scope: ['https://www.googleapis.com/auth/dialogflow'],
80 | lifetime: '3599s',
81 | };
82 |
83 | // post the data
84 | post_req.write(JSON.stringify(body));
85 | post_req.end();
86 | });
87 | }
88 | // [END generate_token]
89 |
90 | // [START retrieve_credentials]
91 | function retrieveCredentials(context) {
92 | return new Promise(resolve => {
93 | // To create a new access token, we first have to retrieve the credentials
94 | // of the service account that will make the generateTokenRequest().
95 | // To do that, we will use the App Engine Default Service Account.
96 | const options = {
97 | host: 'metadata.google.internal',
98 | path: '/computeMetadata/v1/instance/service-accounts/default/token',
99 | method: 'GET',
100 | headers: {'Metadata-Flavor': 'Google'},
101 | };
102 |
103 | const get_req = http.get(options, res => {
104 | let body = '';
105 |
106 | res.on('data', chunk => {
107 | body += chunk;
108 | });
109 |
110 | res.on('end', () => {
111 | const response = JSON.parse(body);
112 | return generateAccessToken(
113 | context,
114 | response.access_token,
115 | response.token_type
116 | ).then(result => {
117 | return resolve(result);
118 | });
119 | });
120 | });
121 | get_req.on('error', e => {
122 | //console.log('Error retrieving credentials', e.message);
123 | return 'Error retrieving token' + e.message;
124 | });
125 | get_req.end();
126 | });
127 | }
128 | exports.retrieveCredentials = retrieveCredentials;
129 | // [END retrieve_credentials]
130 |
131 | // [START validate_token]
132 | // This method verifies the token expiry by validating against current time
133 | function isValid(expiryTime) {
134 | const currentDate = new Date();
135 | const expirationDate = new Date(expiryTime);
136 | // If within 5 minutes of expiration, return false
137 | return currentDate <= expirationDate - 1000 * 60 * 5;
138 | }
139 | // [END validate_token]
140 |
141 | // [START function_get_token]
142 | exports.getOAuthToken = functions.https.onCall((data, context) => {
143 | // Checking that the user is authenticated.
144 | if (!context.auth) {
145 | // Throwing an HttpsError so that the client gets the error details.
146 | throw new functions.https.HttpsError(
147 | 'failed-precondition',
148 | 'The function must be called ' + 'while authenticated.'
149 | );
150 | }
151 | // Retrieve the token from the database
152 | const docRef = db.collection('ShortLivedAuthTokens').doc('OauthToken');
153 |
154 | return docRef
155 | .get()
156 | .then(doc => {
157 | if (doc.exists && isValid(doc.data().expireTime)) {
158 | //push notification
159 | pushNotification(
160 | data['deviceID'],
161 | doc.data().accessToken,
162 | doc.data().expireTime
163 | );
164 | return doc.data();
165 | } else {
166 | return retrieveCredentials(context).then(result => {
167 | console.log('Print result from retrieveCredentials functions');
168 | console.log(result);
169 | pushNotification(
170 | data['deviceID'],
171 | result['accessToken'],
172 | result['expireTime']
173 | );
174 | return result;
175 | });
176 | }
177 | })
178 | .catch(err => {
179 | console.log('Error retrieving token', err);
180 | pushNotification(data['deviceID'], 'Error retrieving token', 'Error');
181 | // return 'Error retrieving token';
182 | return 'Error retrieving token';
183 | });
184 | });
185 | // [END function_get_token]
186 |
187 | //[START pushNotification]
188 | function pushNotification(deviceID, accessToken, expiryDate) {
189 | //Passing the device id of the requested device which has requested for PN
190 | const tokens = [deviceID];
191 | //Push notification payload with expiry date as title and access token as body
192 | //Though payload can be consructed in different ways just for simplicity we had choosen this
193 | const payload = {
194 | notification: {
195 | title: expiryDate,
196 | body: accessToken,
197 | sound: 'default',
198 | badge: '1',
199 | },
200 | };
201 | //triggers push notification to the targeted devices.
202 | return admin.messaging().sendToDevice(tokens, payload);
203 | }
204 | //[END pushNotification]
205 |
--------------------------------------------------------------------------------
/Sources/OAuth2/GoogleCloudMetadataTokenProvider.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Dispatch
16 | import Foundation
17 | #if canImport(FoundationNetworking)
18 | import FoundationNetworking
19 | #endif
20 |
21 | public class GoogleCloudMetadataTokenProvider : TokenProvider {
22 | public func withToken(_ callback: @escaping (Token?, Error?) -> Void) throws {
23 | callback(token, nil)
24 | }
25 |
26 | public var token: Token?
27 |
28 | public init?() {
29 | refresh()
30 | if token == nil {
31 | return nil
32 | }
33 | }
34 |
35 | public func refresh() {
36 | let sem = DispatchSemaphore(value: 0)
37 | let urlString = "http://metadata/computeMetadata/v1/instance/service-accounts/default/token"
38 | let urlComponents = URLComponents(string:urlString)!
39 | var request = URLRequest(url: urlComponents.url!)
40 | request.setValue("Google", forHTTPHeaderField:"Metadata-Flavor")
41 | request.httpMethod = "GET"
42 | let session = URLSession(configuration: URLSessionConfiguration.default)
43 | let task: URLSessionDataTask = session.dataTask(with:request) {
44 | (data, response, error) -> Void in
45 | if let data = data {
46 | let decoder = JSONDecoder()
47 | self.token = try? decoder.decode(Token.self, from: data)
48 | }
49 | sem.signal()
50 | }
51 | task.resume()
52 | _ = sem.wait(timeout: DispatchTime.distantFuture)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/OAuth2/GoogleRefreshTokenProvider.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import Dispatch
17 |
18 | struct OAuth2RefreshCredentials : Codable {
19 | let clientID : String
20 | let clientSecret : String
21 | let refreshToken : String
22 | let tokenType : String
23 | enum CodingKeys: String, CodingKey {
24 | case clientID = "client_id"
25 | case clientSecret = "client_secret"
26 | case refreshToken = "refresh_token"
27 | case tokenType = "type"
28 | }
29 | }
30 |
31 | let accessTokenPath = "https://accounts.google.com/o/oauth2/token"
32 |
33 | public class GoogleRefreshTokenProvider: TokenProvider {
34 | private var credentials : OAuth2RefreshCredentials
35 | public var token: Token?
36 |
37 | public init?(credentialsURL: URL) {
38 | guard let credentialsData = try? Data(contentsOf:credentialsURL) else {
39 | return nil
40 | }
41 | let decoder = JSONDecoder()
42 | guard let credentials = try? decoder.decode(OAuth2RefreshCredentials.self,
43 | from: credentialsData)
44 | else {
45 | return nil
46 | }
47 | self.credentials = credentials
48 | do {
49 | self.token = try exchange()
50 | if self.token == nil {
51 | return nil
52 | }
53 | } catch {
54 | return nil
55 | }
56 | }
57 |
58 | private func exchange() throws -> Token? {
59 | let parameters = [
60 | "client_id": credentials.clientID,
61 | "client_secret": credentials.clientSecret,
62 | "grant_type": "refresh_token",
63 | "refresh_token": credentials.refreshToken]
64 | var responseData: Data?
65 | let sem = DispatchSemaphore(value: 0)
66 | Connection.performRequest(
67 | method: "POST",
68 | urlString: accessTokenPath,
69 | parameters: parameters,
70 | body: nil,
71 | authorization: "") { data, response, _ in
72 | responseData = data
73 | sem.signal()
74 | }
75 | _ = sem.wait(timeout: DispatchTime.distantFuture)
76 | // assume content type is "application/json"
77 | return try JSONDecoder().decode(Token.self, from: responseData!)
78 | }
79 |
80 | public func withToken(_ callback: @escaping (Token?, Error?) -> Void) throws {
81 | callback(token, nil)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/OAuth2/PlatformNativeTokenProvider.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | #if os(macOS) || os(iOS)
16 | import Dispatch
17 | import Foundation
18 | import AuthenticationServices
19 |
20 | public struct NativeCredentials: Codable, CodeExchangeInfo, RefreshExchangeInfo {
21 | let clientID: String
22 | let authorizeURL: String
23 | let accessTokenURL: String
24 | let callbackScheme: String
25 | enum CodingKeys: String, CodingKey {
26 | case clientID = "client_id"
27 | case authorizeURL = "authorize_url"
28 | case accessTokenURL = "access_token_url"
29 | case callbackScheme = "callback_scheme"
30 | }
31 | var redirectURI: String {
32 | callbackScheme + ":/oauth2redirect"
33 | }
34 | var clientSecret: String {
35 | ""
36 | }
37 | }
38 |
39 | @available(macOS 10.15.4, iOS 13.4, *)
40 | public class PlatformNativeTokenProvider: TokenProvider {
41 | private var credentials: NativeCredentials
42 | private var session: Session?
43 | public var token: Token?
44 |
45 | // for parity with BrowserTokenProvider
46 | public convenience init?(credentials: String, token tokenfile: String) {
47 | let path = ProcessInfo.processInfo.environment["HOME"]!
48 | + "/.credentials/" + credentials
49 | let url = URL(fileURLWithPath: path)
50 |
51 | guard let credentialsData = try? Data(contentsOf: url) else {
52 | print("No credentials data at \(path).")
53 | return nil
54 | }
55 | self.init(credentials: credentialsData, token: tokenfile)
56 | }
57 |
58 | public init?(credentials: Data, token tokenfile: String) {
59 | let decoder = JSONDecoder()
60 | guard let credentials = try? decoder.decode(NativeCredentials.self,
61 | from: credentials)
62 | else {
63 | print("Error reading credentials")
64 | return nil
65 | }
66 | self.credentials = credentials
67 |
68 | if tokenfile != "" {
69 | do {
70 | let data = try Data(contentsOf: URL(fileURLWithPath: tokenfile))
71 | let decoder = JSONDecoder()
72 | guard let token = try? decoder.decode(Token.self, from: data)
73 | else {
74 | return nil
75 | }
76 | self.token = token
77 | } catch {
78 | // ignore errors due to missing token files
79 | }
80 | }
81 | }
82 |
83 | public func saveToken(_ filename: String) throws {
84 | if let token = token {
85 | try token.save(filename)
86 | }
87 | }
88 |
89 | public func refreshToken(_ filename: String) throws {
90 | if let token = token, token.isExpired() {
91 | self.token = try Refresh(token: token).exchange(info: credentials)
92 | try saveToken(filename)
93 | }
94 | }
95 |
96 | private struct Session {
97 | let webAuthSession: ASWebAuthenticationSession
98 | let webAuthContext: ASWebAuthenticationPresentationContextProviding
99 | }
100 |
101 | // The presentation context provides a reference to a UIWindow that the auth
102 | // framework uese to display the confirmation modal and sign in controller.
103 | public func signIn(
104 | scopes: [String],
105 | context: ASWebAuthenticationPresentationContextProviding,
106 | completion: @escaping (Token?, AuthError?) -> Void
107 | ) {
108 | let state = UUID().uuidString
109 | let scope = scopes.joined(separator: " ")
110 | var urlComponents = URLComponents(string: credentials.authorizeURL)!
111 | urlComponents.queryItems = [
112 | URLQueryItem(name: "client_id", value: credentials.clientID),
113 | URLQueryItem(name: "response_type", value: "code"),
114 | URLQueryItem(name: "redirect_uri", value: credentials.redirectURI),
115 | URLQueryItem(name: "state", value: state),
116 | URLQueryItem(name: "scope", value: scope),
117 | ]
118 |
119 | let session = ASWebAuthenticationSession(
120 | url: urlComponents.url!,
121 | callbackURLScheme: credentials.callbackScheme
122 | ) { url, err in
123 | defer { self.session = nil }
124 | if let e = err {
125 | completion(nil, .webSession(inner: e))
126 | return
127 | }
128 | guard let u = url else {
129 | // If err is nil, url should not be, and vice versa.
130 | completion(nil, .unknownError)
131 | return
132 | }
133 | let code = Code(urlComponents: URLComponents(string: u.absoluteString)!)
134 | do {
135 | self.token = try code.exchange(info: self.credentials)
136 | completion(self.token!, nil)
137 | } catch let ae as AuthError {
138 | completion(nil, ae)
139 | } catch {
140 | completion(nil, .unknownError)
141 | }
142 | }
143 |
144 | session.presentationContextProvider = context
145 | if !session.canStart {
146 | // This happens if the context provider is not set, or if the session has
147 | // already been started. We enforce correct usage so ignore.
148 | return
149 | }
150 | let success = session.start()
151 | if !success {
152 | // This doesn't happen unless the context is not set, disappears (it's a
153 | // weak ref internally), or the session was previously started, ignore.
154 | return
155 | }
156 | self.session = Session(webAuthSession: session, webAuthContext: context)
157 | }
158 |
159 | // Canceling the session dismisses the view controller if it is showing.
160 | public func cancelSignIn() {
161 | session?.webAuthSession.cancel()
162 | self.session = nil
163 | }
164 |
165 | public func withToken(_ callback: @escaping (Token?, Error?) -> Void) throws {
166 | callback(token, nil)
167 | }
168 | }
169 | #endif
170 |
--------------------------------------------------------------------------------
/Sources/OAuth2/Refresh.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | #if canImport(FoundationNetworking)
17 | import FoundationNetworking
18 | #endif
19 |
20 | protocol RefreshExchangeInfo {
21 | var accessTokenURL: String { get }
22 | var clientID: String { get }
23 | var clientSecret: String { get }
24 | }
25 |
26 | class Refresh {
27 |
28 | let token: String
29 |
30 | convenience init(token data: Data) throws {
31 | let token = try JSONDecoder().decode(Token.self, from: data)
32 | try self.init(token: token)
33 | }
34 |
35 | init(token: Token) throws {
36 | if let rt = token.RefreshToken {
37 | self.token = rt
38 | } else {
39 | throw AuthError.noRefreshToken
40 | }
41 | }
42 |
43 | func exchange(info: RefreshExchangeInfo) throws -> Token {
44 | let sem = DispatchSemaphore(value: 0)
45 | let parameters = [
46 | "client_id": info.clientID,
47 | "client_secret": info.clientSecret,
48 | "grant_type": "refresh_token",
49 | "refresh_token": self.token,
50 | ]
51 | let token = info.clientID + ":" + info.clientSecret
52 | // some providers require the client id and secret in the authorization header
53 | let authorization = "Basic " + String(data: token.data(using: .utf8)!.base64EncodedData(), encoding: .utf8)!
54 | var responseData: Data?
55 | var contentType: String?
56 | Connection.performRequest(
57 | method: "POST",
58 | urlString: info.accessTokenURL,
59 | parameters: parameters,
60 | body: nil,
61 | authorization: authorization
62 | ) { data, response, _ in
63 | if let response = response as? HTTPURLResponse {
64 | for (k, v) in response.allHeaderFields {
65 | // Explicitly-lowercasing seems like it should be unnecessary,
66 | // but some services returned "Content-Type" and others sent "content-type".
67 | if (k as! String).lowercased() == "content-type" {
68 | contentType = v as? String
69 | }
70 | }
71 | }
72 | responseData = data
73 | sem.signal()
74 | }
75 | _ = sem.wait(timeout: DispatchTime.distantFuture)
76 |
77 | guard let responseData = responseData else {
78 | throw AuthError.unknownError
79 | }
80 |
81 | if let contentType = contentType, contentType.contains("application/json") {
82 | let decoder = JSONDecoder()
83 | var token = try decoder.decode(Token.self, from: responseData)
84 |
85 | if token.CreationTime == nil {
86 | token.CreationTime = Date()
87 | }
88 |
89 | if token.RefreshToken == nil {
90 | // Google refresh tokens are persistent
91 | token.RefreshToken = self.token
92 | }
93 |
94 | return token
95 | } else { // assume "application/x-www-form-urlencoded"
96 | guard let queryParameters = String(data: responseData, encoding: .utf8) else {
97 | throw AuthError.unknownError
98 | }
99 | guard let urlComponents = URLComponents(string: "http://example.com?" + queryParameters) else {
100 | throw AuthError.unknownError
101 | }
102 | var token = Token(urlComponents: urlComponents)
103 | if token.RefreshToken == nil {
104 | // Google refresh tokens are persistent
105 | token.RefreshToken = self.token
106 | }
107 | return token
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Sources/OAuth2/ServiceAccountTokenProvider/ASN1.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import CryptoSwift
17 | import BigInt
18 | import SwiftyBase64
19 |
20 | private extension Data {
21 | var firstByte: UInt8 {
22 | var byte: UInt8 = 0
23 | copyBytes(to: &byte, count: MemoryLayout.size)
24 | return byte
25 | }
26 | }
27 |
28 | private class ASN1Scanner {
29 | let data: Data
30 |
31 | private(set) var position = 0
32 |
33 | init(data: Data) {
34 | self.data = data
35 | }
36 |
37 | var isComplete: Bool {
38 | return position >= data.count
39 | }
40 |
41 | var remaining: Int {
42 | return data.count - position
43 | }
44 |
45 | func scan(distance: Int) -> Data? {
46 | guard distance > 0 else { return nil }
47 | guard position <= (data.count - distance) else { return nil }
48 | defer {
49 | position = position + distance
50 | }
51 | return data.subdata(in: data.startIndex.advanced(by: position).. Int {
55 | var length = 0
56 | if let lengthByte = self.scan(distance: 1)?.firstByte {
57 | if lengthByte & 0x80 != 0x00 {
58 | // long form
59 | let lengthLength = Int(lengthByte & 0x7F)
60 | for _ in 0.. [DERCode] {
90 | return [
91 | .Boolean,
92 | .Integer,
93 | .OctetString,
94 | .Null,
95 | .ObjectIdentifier,
96 | .IA5String,
97 | .Sequence,
98 | ]
99 | }
100 | }
101 | static func decode(der: Data) -> [ASN1Object]? {
102 | let scanner = ASN1Scanner(data: der)
103 | //Verify that this is actually a DER sequence
104 | guard scanner.scan(distance: 1)?.firstByte == DERCode.Sequence.rawValue else {
105 | print("not a DER sequence")
106 | return nil
107 | }
108 | let length = scanner.scanLength()
109 | // The length should be length of the data, minus itself and the sequence type
110 | guard length == scanner.remaining else {
111 | print("invalid length \(length)")
112 | return nil
113 | }
114 | return self.decodeSequence(scanner: scanner)
115 | }
116 | fileprivate static func decodeSequence(scanner: ASN1Scanner) -> [ASN1Object]? {
117 | var output: [ASN1Object] = []
118 | while !scanner.isComplete {
119 | let scannedType = scanner.scan(distance: 1)?.firstByte
120 | var dataType: DERCode?
121 | for type in DERCode.allTypes() {
122 | if scannedType == type.rawValue {
123 | dataType = type
124 | break
125 | }
126 | }
127 | guard let type = dataType else {
128 | let unsupported = scannedType
129 | print("unsupported type \(unsupported!)")
130 | return nil
131 | }
132 | let fieldLength = scanner.scanLength()
133 | if fieldLength > 0 {
134 | guard let actualData = scanner.scan(distance: fieldLength) else {
135 | print("too few bytes")
136 | return nil
137 | }
138 | var object = ASN1Object(type: type, data: actualData, children:nil)
139 | if type == .Sequence {
140 | object.children = decodeSequence(scanner: ASN1Scanner(data: actualData))
141 | }
142 | if type == .OctetString {
143 | object.children = decode(der:actualData)
144 | }
145 | output.append(object)
146 | } else {
147 | output.append(ASN1Object(type: type, data: Data(), children:nil))
148 | }
149 | }
150 | return output
151 | }
152 | }
153 |
154 |
155 |
--------------------------------------------------------------------------------
/Sources/OAuth2/ServiceAccountTokenProvider/JWT.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import CryptoSwift
17 | import SwiftyBase64
18 |
19 | struct JWTHeader : Codable {
20 | let Algorithm : String
21 | let Format : String
22 | enum CodingKeys: String, CodingKey {
23 | case Algorithm = "alg"
24 | case Format = "typ"
25 | }
26 | }
27 |
28 | struct JWTClaimSet : Codable {
29 | let Issuer : String
30 | let Audience : String
31 | let Scope : String
32 | let IssuedAt : Int
33 | let Expiration : Int
34 | enum CodingKeys: String, CodingKey {
35 | case Issuer = "iss"
36 | case Audience = "aud"
37 | case Scope = "scope"
38 | case IssuedAt = "iat"
39 | case Expiration = "exp"
40 | }
41 | }
42 |
43 | struct JWT {
44 | static func encodeWithRS256(jwtHeader: JWTHeader,
45 | jwtClaimSet: JWTClaimSet,
46 | rsaKey: RSAKey) throws -> String {
47 | let encoder = JSONEncoder()
48 | let headerData = try encoder.encode(jwtHeader)
49 | let claimsData = try encoder.encode(jwtClaimSet)
50 | let header = SwiftyBase64.EncodeString(Array(headerData), alphabet:.URLAndFilenameSafe)
51 | let claims = SwiftyBase64.EncodeString(Array(claimsData), alphabet:.URLAndFilenameSafe)
52 | let body = header + "." + claims
53 | let bodyData = body.data(using: String.Encoding.utf8)!
54 | let sha2 = SHA2(variant: SHA2.Variant(rawValue:256)!)
55 | let hash = sha2.calculate(for:Array(bodyData))
56 | let signature = rsaKey.sign(hash:hash)
57 | let signatureString = SwiftyBase64.EncodeString(signature, alphabet:.URLAndFilenameSafe)
58 | return body + "." + signatureString
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/OAuth2/ServiceAccountTokenProvider/RSA.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | import BigInt
17 |
18 | struct RSAKey {
19 | var N : BigUInt
20 | var E : BigUInt
21 | var D : BigUInt
22 | /*
23 | var Primes : [BigUInt]
24 | var Dp: BigUInt
25 | var Dq: BigUInt
26 | var Qinv : BigUInt
27 | */
28 |
29 | init?(privateKey:String) {
30 | var pem = ""
31 | for line in privateKey.components(separatedBy:"\n") {
32 | if line.range(of:"PRIVATE KEY") == nil {
33 | pem += line
34 | }
35 | }
36 | if let der = Data(base64Encoded: pem, options: []),
37 | let asn1Array = ASN1Decoder.decode(der: der) {
38 | N = BigUInt(asn1Array[2].children?[1].data ?? Data())
39 | E = BigUInt(asn1Array[2].children?[2].data ?? Data())
40 | D = BigUInt(asn1Array[2].children?[3].data ?? Data())
41 | } else {
42 | return nil
43 | }
44 | }
45 |
46 | func sign(hash:[UInt8]) -> [UInt8] {
47 | let prefix : [UInt8] = [0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20]
48 | var dataToEncode = [UInt8](repeating: 0xFF, count: 256)
49 | dataToEncode[0] = 0x00
50 | dataToEncode[1] = 0x01
51 | let offset1 = 256 - hash.count - prefix.count
52 | dataToEncode[offset1 - 1] = 0x00
53 | for i in 0.. Void) throws {
76 |
77 | // leave spare at least one second :)
78 | if let token = token, token.timeToExpiry() > 1 {
79 | callback(token, nil)
80 | return
81 | }
82 |
83 | let iat = Date()
84 | let exp = iat.addingTimeInterval(3600)
85 | let jwtClaimSet = JWTClaimSet(Issuer:credentials.ClientEmail,
86 | Audience:credentials.TokenURI,
87 | Scope: scopes.joined(separator: " "),
88 | IssuedAt: Int(iat.timeIntervalSince1970),
89 | Expiration: Int(exp.timeIntervalSince1970))
90 | let jwtHeader = JWTHeader(Algorithm: "RS256",
91 | Format: "JWT")
92 | let msg = try JWT.encodeWithRS256(jwtHeader:jwtHeader,
93 | jwtClaimSet:jwtClaimSet,
94 | rsaKey:rsaKey)
95 | let json: [String: Any] = ["grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
96 | "assertion": msg]
97 | let data = try JSONSerialization.data(withJSONObject: json)
98 |
99 | var urlRequest = URLRequest(url:URL(string:credentials.TokenURI)!)
100 | urlRequest.httpMethod = "POST"
101 | urlRequest.httpBody = data
102 | urlRequest.setValue("application/json", forHTTPHeaderField:"Content-Type")
103 |
104 | let session = URLSession(configuration: URLSessionConfiguration.default)
105 | let task: URLSessionDataTask = session.dataTask(with:urlRequest)
106 | {(data, response, error) -> Void in
107 | if let data = data,
108 | let token = try? JSONDecoder().decode(Token.self, from: data) {
109 | self.token = token
110 | self.token?.CreationTime = Date()
111 | callback(self.token, error)
112 | } else {
113 | callback(nil, error)
114 | }
115 | }
116 | task.resume()
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/Sources/OAuth2/Token.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | import Foundation
15 |
16 | public struct Token : Codable {
17 | public var AccessToken : String?
18 | public var TokenType : String?
19 | public var ExpiresIn : Int?
20 | public var RefreshToken : String?
21 | public var Scope : String?
22 | public var CreationTime : Date?
23 | enum CodingKeys: String, CodingKey {
24 | case AccessToken = "access_token"
25 | case TokenType = "token_type"
26 | case ExpiresIn = "expires_in"
27 | case RefreshToken = "refresh_token"
28 | case Scope = "scope"
29 | case CreationTime = "creation_time"
30 | }
31 |
32 | func save(_ filename: String) throws {
33 | let encoder = JSONEncoder()
34 | let data = try encoder.encode(self)
35 | try data.write(to: URL(fileURLWithPath: filename))
36 | }
37 |
38 | public func isExpired() -> Bool {
39 | return timeToExpiry() <= 0
40 | }
41 |
42 | public func timeToExpiry() -> TimeInterval {
43 | guard let expiresIn = ExpiresIn, let creationTime = CreationTime else {
44 | return 0.0 // if we dont know when it expires, assume its expired
45 | }
46 | let expireDate = creationTime.addingTimeInterval(TimeInterval(expiresIn))
47 | return expireDate.timeIntervalSinceNow
48 | }
49 |
50 | public init(accessToken: String) {
51 | self.AccessToken = accessToken
52 | }
53 |
54 | public init(urlComponents: URLComponents) {
55 | CreationTime = Date()
56 | for queryItem in urlComponents.queryItems! {
57 | if let value = queryItem.value {
58 | switch queryItem.name {
59 | case "access_token":
60 | AccessToken = value
61 | case "token_type":
62 | TokenType = value
63 | case "expires_in":
64 | ExpiresIn = Int(value)
65 | case "refresh_token":
66 | RefreshToken = value
67 | case "scope":
68 | Scope = value
69 | default:
70 | break
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/OAuth2/TokenProvider.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | public protocol TokenProvider {
16 | func withToken(_ callback:@escaping (Token?, Error?) -> Void) throws
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/OAuth2/openURL.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import Foundation
16 | #if os(OSX)
17 | import Cocoa
18 | #elseif os(iOS) || os(tvOS)
19 | import UIKit
20 | #endif
21 |
22 | #if !(os(iOS) || os(tvOS))
23 | private func shell(_ args: String...) -> Int32 {
24 | let task = Process()
25 | if #available(macOS 10.13, *) {
26 | task.executableURL = URL(string: "/usr/bin/env")
27 | } else {
28 | task.launchPath = "/usr/bin/env"
29 | }
30 | task.arguments = args
31 | if #available(macOS 10.13, *) {
32 | do {
33 | try task.run()
34 | } catch {
35 | return -1
36 | }
37 | } else {
38 | task.launch()
39 | }
40 | task.waitUntilExit()
41 | return task.terminationStatus
42 | }
43 | #endif
44 |
45 | @available(iOS 10.0, tvOS 10.0, *)
46 | internal func openURL(_ url: URL) {
47 | #if os(OSX)
48 | if !NSWorkspace.shared.open(url) {
49 | print("default browser could not be opened")
50 | }
51 | #elseif os(iOS) || os(tvOS)
52 | UIApplication.shared.open(url) { success in
53 | if !success {
54 | print("default browser could not be opened")
55 | }
56 | }
57 | #else // Linux, tested on Ubuntu
58 | let status = shell("xdg-open", String(describing:url))
59 | if status != 0 {
60 | print("To continue, please open this URL in your browser: (\(String(describing:url)))")
61 | }
62 | #endif
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/SwiftyBase64/Alphabets.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Alphabets.swift
3 | // SwiftyBase64
4 | //
5 | // Created by Doug Richardson on 8/7/15.
6 | //
7 | //
8 |
9 | // The tables in this file were generated using generate_alphabet_table in the tools directory.
10 | // Note the tables contain 65 characters: 64 to do the translation and 1 more for the padding
11 | // character used in each alphabet.
12 |
13 | /// Standard Base64 encoding table.
14 | let StandardAlphabet : [UInt8] = [
15 | 65, // 0=A
16 | 66, // 1=B
17 | 67, // 2=C
18 | 68, // 3=D
19 | 69, // 4=E
20 | 70, // 5=F
21 | 71, // 6=G
22 | 72, // 7=H
23 | 73, // 8=I
24 | 74, // 9=J
25 | 75, // 10=K
26 | 76, // 11=L
27 | 77, // 12=M
28 | 78, // 13=N
29 | 79, // 14=O
30 | 80, // 15=P
31 | 81, // 16=Q
32 | 82, // 17=R
33 | 83, // 18=S
34 | 84, // 19=T
35 | 85, // 20=U
36 | 86, // 21=V
37 | 87, // 22=W
38 | 88, // 23=X
39 | 89, // 24=Y
40 | 90, // 25=Z
41 | 97, // 26=a
42 | 98, // 27=b
43 | 99, // 28=c
44 | 100, // 29=d
45 | 101, // 30=e
46 | 102, // 31=f
47 | 103, // 32=g
48 | 104, // 33=h
49 | 105, // 34=i
50 | 106, // 35=j
51 | 107, // 36=k
52 | 108, // 37=l
53 | 109, // 38=m
54 | 110, // 39=n
55 | 111, // 40=o
56 | 112, // 41=p
57 | 113, // 42=q
58 | 114, // 43=r
59 | 115, // 44=s
60 | 116, // 45=t
61 | 117, // 46=u
62 | 118, // 47=v
63 | 119, // 48=w
64 | 120, // 49=x
65 | 121, // 50=y
66 | 122, // 51=z
67 | 48, // 52=0
68 | 49, // 53=1
69 | 50, // 54=2
70 | 51, // 55=3
71 | 52, // 56=4
72 | 53, // 57=5
73 | 54, // 58=6
74 | 55, // 59=7
75 | 56, // 60=8
76 | 57, // 61=9
77 | 43, // 62=+
78 | 47, // 63=/
79 | // PADDING FOLLOWS, not used during lookups
80 | 61, // 64==
81 | ]
82 |
83 | /// URL and Filename Safe Base64 encoding table.
84 | let URLAndFilenameSafeAlphabet : [UInt8] = [
85 | 65, // 0=A
86 | 66, // 1=B
87 | 67, // 2=C
88 | 68, // 3=D
89 | 69, // 4=E
90 | 70, // 5=F
91 | 71, // 6=G
92 | 72, // 7=H
93 | 73, // 8=I
94 | 74, // 9=J
95 | 75, // 10=K
96 | 76, // 11=L
97 | 77, // 12=M
98 | 78, // 13=N
99 | 79, // 14=O
100 | 80, // 15=P
101 | 81, // 16=Q
102 | 82, // 17=R
103 | 83, // 18=S
104 | 84, // 19=T
105 | 85, // 20=U
106 | 86, // 21=V
107 | 87, // 22=W
108 | 88, // 23=X
109 | 89, // 24=Y
110 | 90, // 25=Z
111 | 97, // 26=a
112 | 98, // 27=b
113 | 99, // 28=c
114 | 100, // 29=d
115 | 101, // 30=e
116 | 102, // 31=f
117 | 103, // 32=g
118 | 104, // 33=h
119 | 105, // 34=i
120 | 106, // 35=j
121 | 107, // 36=k
122 | 108, // 37=l
123 | 109, // 38=m
124 | 110, // 39=n
125 | 111, // 40=o
126 | 112, // 41=p
127 | 113, // 42=q
128 | 114, // 43=r
129 | 115, // 44=s
130 | 116, // 45=t
131 | 117, // 46=u
132 | 118, // 47=v
133 | 119, // 48=w
134 | 120, // 49=x
135 | 121, // 50=y
136 | 122, // 51=z
137 | 48, // 52=0
138 | 49, // 53=1
139 | 50, // 54=2
140 | 51, // 55=3
141 | 52, // 56=4
142 | 53, // 57=5
143 | 54, // 58=6
144 | 55, // 59=7
145 | 56, // 60=8
146 | 57, // 61=9
147 | 45, // 62=-
148 | 95, // 63=_
149 | // PADDING FOLLOWS, not used during lookups
150 | 61, // 64==
151 | ]
152 |
--------------------------------------------------------------------------------
/Sources/SwiftyBase64/Base64.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Base64.swift
3 | // SwiftyBase64
4 | //
5 | // Created by Doug Richardson on 8/7/15.
6 | //
7 | //
8 |
9 | /**
10 | Base64 Alphabet to use during encoding.
11 |
12 | - Standard: The standard Base64 encoding, defined in RFC 4648 section 4.
13 | - URLAndFilenameSafe: The base64url encoding, defined in RFC 4648 section 5.
14 | */
15 | public enum Alphabet {
16 | /// The standard Base64 alphabet
17 | case Standard
18 |
19 | /// The URL and Filename Safe Base64 alphabet
20 | case URLAndFilenameSafe
21 | }
22 |
23 | /**
24 | Encode a [UInt8] byte array as a Base64 String.
25 |
26 | - parameter bytes: Bytes to encode.
27 | - parameter alphabet: The Base64 alphabet to encode with.
28 | - returns: A String of the encoded bytes.
29 | */
30 | public func EncodeString(_ bytes : [UInt8], alphabet : Alphabet = .Standard) -> String {
31 | let encoded = Encode(bytes, alphabet : alphabet)
32 | var result = String()
33 | for b in encoded {
34 | result.append(String(UnicodeScalar(b)))
35 | }
36 | return result
37 | }
38 |
39 | /// Get the encoding table for the alphabet.
40 | private func tableForAlphabet(_ alphabet : Alphabet) -> [UInt8] {
41 | switch alphabet {
42 | case .Standard:
43 | return StandardAlphabet
44 | case .URLAndFilenameSafe:
45 | return URLAndFilenameSafeAlphabet
46 | }
47 | }
48 |
49 | /**
50 | Use the Base64 algorithm as decribed by RFC 4648 section 4 to
51 | encode the input bytes. The alphabet specifies the translation
52 | table to use. RFC 4648 defines two such alphabets:
53 |
54 | - Standard (section 4)
55 | - URL and Filename Safe (section 5)
56 |
57 | - parameter bytes: Bytes to encode.
58 | - parameter alphabet: The Base64 alphabet to encode with.
59 | - returns: Base64 encoded ASCII bytes.
60 | */
61 | public func Encode(_ bytes : [UInt8], alphabet : Alphabet = .Standard) -> [UInt8] {
62 | var encoded : [UInt8] = []
63 |
64 | let table = tableForAlphabet(alphabet)
65 | let padding = table[64]
66 |
67 | var i = 0
68 | let count = bytes.count
69 |
70 | while i+3 <= count {
71 | let one = bytes[i] >> 2
72 | let two = ((bytes[i] & 0b11) << 4) | ((bytes[i+1] & 0b11110000) >> 4)
73 | let three = ((bytes[i+1] & 0b00001111) << 2) | ((bytes[i+2] & 0b11000000) >> 6)
74 | let four = bytes[i+2] & 0b00111111
75 |
76 | encoded.append(table[Int(one)])
77 | encoded.append(table[Int(two)])
78 | encoded.append(table[Int(three)])
79 | encoded.append(table[Int(four)])
80 |
81 | i += 3
82 | }
83 |
84 | if i+2 == count {
85 | // (3) The final quantum of encoding input is exactly 16 bits; here, the
86 | // final unit of encoded output will be three characters followed by
87 | // one "=" padding character.
88 | let one = bytes[i] >> 2
89 | let two = ((bytes[i] & 0b11) << 4) | ((bytes[i+1] & 0b11110000) >> 4)
90 | let three = ((bytes[i+1] & 0b00001111) << 2)
91 | encoded.append(table[Int(one)])
92 | encoded.append(table[Int(two)])
93 | encoded.append(table[Int(three)])
94 | encoded.append(padding)
95 | } else if i+1 == count {
96 | // (2) The final quantum of encoding input is exactly 8 bits; here, the
97 | // final unit of encoded output will be two characters followed by
98 | // two "=" padding characters.
99 | let one = bytes[i] >> 2
100 | let two = ((bytes[i] & 0b11) << 4)
101 | encoded.append(table[Int(one)])
102 | encoded.append(table[Int(two)])
103 | encoded.append(padding)
104 | encoded.append(padding)
105 | } else {
106 | // (1) The final quantum of encoding input is an integral multiple of 24
107 | // bits; here, the final unit of encoded output will be an integral
108 | // multiple of 4 characters with no "=" padding.
109 | assert(i == count)
110 | }
111 |
112 | return encoded
113 | }
114 |
--------------------------------------------------------------------------------
/Sources/SwiftyBase64/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Doug Richardson
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 |
23 |
--------------------------------------------------------------------------------
/Sources/TinyHTTPServer/TinyHTTPServer.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import NIO
16 | import NIOHTTP1
17 |
18 | public typealias TinyHTTPHandler =
19 | (TinyHTTPServer, HTTPRequestHead) -> (String, HTTPResponseStatus)
20 |
21 | public class TinyHTTPServer {
22 |
23 | private final class HTTPHandler: ChannelInboundHandler {
24 | public typealias InboundIn = HTTPServerRequestPart
25 | public typealias OutboundOut = HTTPServerResponsePart
26 |
27 | private var server : TinyHTTPServer
28 | private var handler : TinyHTTPHandler
29 |
30 | init(server : TinyHTTPServer, handler: @escaping TinyHTTPHandler) {
31 | self.server = server
32 | self.handler = handler
33 | }
34 |
35 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
36 | switch self.unwrapInboundIn(data) {
37 | case .head(let request):
38 | let (response, code) = self.handler(self.server, request)
39 | // write header of response
40 | context.writeAndFlush(self.wrapOutboundOut(
41 | .head(HTTPResponseHead(version: request.version,
42 | status: code,
43 | headers: HTTPHeaders()))),
44 | promise:nil)
45 | // write body of response
46 | var buf = context.channel.allocator.buffer(capacity: response.utf8.count)
47 | buf.writeString(response)
48 | context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(buf))), promise: nil)
49 | // write end of response
50 | let promise : EventLoopPromise = context.eventLoop.makePromise()
51 | promise.futureResult.whenComplete {
52 | (_: Result) in context.close(promise: nil) }
53 |
54 | context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: promise)
55 | default:
56 | break
57 | }
58 | }
59 | }
60 |
61 | var channel : Channel!
62 | var group : MultiThreadedEventLoopGroup!
63 | var threadPool : NIOThreadPool!
64 |
65 | public init() {
66 | }
67 |
68 | public func start(handler: @escaping TinyHTTPHandler) throws {
69 | let host = "::1"
70 | let port = 8080
71 | self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
72 | self.threadPool = NIOThreadPool(numberOfThreads: 1)
73 | threadPool.start()
74 | let bootstrap = ServerBootstrap(group: group)
75 | .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
76 | .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
77 | .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
78 | .childChannelInitializer { channel in
79 | channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap {
80 | channel.pipeline.addHandler(HTTPHandler(server: self, handler: handler))
81 | }
82 | }
83 |
84 | channel = try bootstrap.bind(host: host, port: port).wait()
85 | if let localAddress = channel.localAddress {
86 | print("TinyHTTPServer started and listening on \(localAddress).")
87 | }
88 | }
89 |
90 | public func stop() {
91 | MultiThreadedEventLoopGroup.currentEventLoop!.scheduleTask(in: .seconds(0)) {
92 | _ = self.channel.close()
93 | print("TinyHTTPServer stopped.")
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/credentials/README.md:
--------------------------------------------------------------------------------
1 | # Sample credentials
2 |
3 | This directory contains sample application credentials for various services.
4 |
5 | To use a service, sign up with the service provider, register an app, and
6 | put your app's consumer key and consumer secret (OAuth1) or client id and
7 | client secret (OAuth2) in the corresponding JSON file.
8 |
9 | Then move that JSON file into a directory named `.credentials` in your home
10 | directory.
11 |
--------------------------------------------------------------------------------
/credentials/github.json:
--------------------------------------------------------------------------------
1 | {
2 | "client_id": "",
3 | "client_secret": "",
4 | "authorize_url": "https://github.com/login/oauth/authorize",
5 | "access_token_url": "https://github.com/login/oauth/access_token",
6 | "callback": "/github/callback"
7 | }
8 |
--------------------------------------------------------------------------------
/credentials/google.json:
--------------------------------------------------------------------------------
1 | {
2 | "client_id": "",
3 | "client_secret": "",
4 | "authorize_url": "https://accounts.google.com/o/oauth2/auth",
5 | "access_token_url": "https://accounts.google.com/o/oauth2/token",
6 | "callback": "/google/callback"
7 | }
8 |
--------------------------------------------------------------------------------
/credentials/meetup.json:
--------------------------------------------------------------------------------
1 | {
2 | "client_id": "",
3 | "client_secret": "",
4 | "authorize_url": "https://secure.meetup.com/oauth2/authorize",
5 | "access_token_url": "https://secure.meetup.com/oauth2/access",
6 | "callback": "/meetup/callback"
7 | }
8 |
--------------------------------------------------------------------------------
/credentials/spotify.json:
--------------------------------------------------------------------------------
1 | {
2 | "client_id": "",
3 | "client_secret": "",
4 | "authorize_url": "https://accounts.spotify.com/authorize",
5 | "access_token_url": "https://accounts.spotify.com/api/token",
6 | "callback": "/spotify/callback"
7 | }
8 |
--------------------------------------------------------------------------------
/credentials/twitter.json:
--------------------------------------------------------------------------------
1 | {
2 | "consumer_key": "",
3 | "consumer_secret": "",
4 | "request_token_url": "https://api.twitter.com/oauth/request_token",
5 | "authorize_url": "https://api.twitter.com/oauth/authorize",
6 | "access_token_url": "https://api.twitter.com/oauth/access_token",
7 | "callback": "/twitter/callback"
8 | }
9 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------