├── .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 | [![Swift Actions Status](https://github.com/googleapis/google-auth-library-swift/workflows/Swift/badge.svg)](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 | --------------------------------------------------------------------------------