diff --git a/.swift-version b/.swift-version index bf77d549..a75b92f1 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.2 +5.1 diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 27154b27..5209b3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,32 @@ Changelog Version numbering represents the Swift version, plus a running number representing updates, fixes and new features at the same time. You can also refer to commit logs to get details on what was implemented, fixed and improved. -### Master +### 5.2.0 + +- Separate setting for `refresh_uri`. + [fotiDim](https://github.com/fotiDim) + [#330](https://github.com/p2/OAuth2/pull/330) +- Add Mac Catalyst support. + [telipskiy](https://github.com/telipskiy) + [#328](https://github.com/p2/OAuth2/pull/328) +- Add PKCE support. + [larrybrunet](https://github.com/larrybrunet) + [#324](https://github.com/p2/OAuth2/pull/324) + +### 5.1.0 + +- Update Swift package configuration for use with XCode 11. + +### 5.0.0 + +- Swift 5.0 support. + [drdavec](https://github.com/drdavec) + [#313](https://github.com/p2/OAuth2/pull/313) +- Add support for Authentication Session. + [blork](https://github.com/blork) + [#305](https://github.com/p2/OAuth2/pull/305) + +### 4.2.0 - Swift 4.2 support. [djbe](https://github.com/djbe) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5e9012ef..23786eda 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -3,8 +3,14 @@ Contributors Contributors to the codebase, in reverse chronological order: +- Foti Dim, @fotidim +- Denis, @telipskiy +- Larry Brunet, @larrybrunet +- Dave Carlson, @drdavec +- Sam Oakley, @blork - David Jennes, @davidjennes - Tim Schmitz, @tschmitz +- Maxime Le Moine, @MaximeLM - Seb Skuse, @sebskuse - David Hardiman, @dhardiman - Amaury David, @amaurydavid diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..f2530a31 --- /dev/null +++ b/Gemfile @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# The bare minimum for building, e.g. in Homebrew +group :build do +end + +# In addition to :build, for contributing +group :development do +end + +# For releasing to GitHub +group :release do + gem 'cocoapods', '~> 1.6.0.beta.2' + gem 'jazzy' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..218b4b5e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,103 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.0) + activesupport (4.2.11) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + atomos (0.1.3) + claide (1.0.2) + cocoapods (1.6.0.beta.2) + activesupport (>= 4.0.2, < 5) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.6.0.beta.2) + cocoapods-deintegrate (>= 1.0.2, < 2.0) + cocoapods-downloader (>= 1.2.2, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-stats (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.3.1, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (~> 2.0.1) + gh_inspector (~> 1.0) + molinillo (~> 0.6.6) + nap (~> 1.0) + ruby-macho (~> 1.3, >= 1.3.1) + xcodeproj (>= 1.7.0, < 2.0) + cocoapods-core (1.6.0.beta.2) + activesupport (>= 4.0.2, < 6) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + cocoapods-deintegrate (1.0.2) + cocoapods-downloader (1.2.2) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.0) + cocoapods-stats (1.0.0) + cocoapods-trunk (1.3.1) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.1.0) + colored2 (3.1.2) + concurrent-ruby (1.1.4) + escape (0.0.4) + ffi (1.10.0) + fourflusher (2.0.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jazzy (0.9.4) + cocoapods (~> 1.0) + mustache (~> 0.99) + open4 + redcarpet (~> 3.2) + rouge (>= 2.0.6, < 4.0) + sass (~> 3.4) + sqlite3 (~> 1.3) + xcinvoke (~> 0.3.0) + liferaft (0.0.6) + minitest (5.11.3) + molinillo (0.6.6) + mustache (0.99.8) + nanaimo (0.2.6) + nap (1.1.0) + netrc (0.11.0) + open4 (1.3.4) + rb-fsevent (0.10.3) + rb-inotify (0.10.0) + ffi (~> 1.0) + redcarpet (3.4.0) + rouge (3.3.0) + ruby-macho (1.3.1) + sass (3.7.3) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sqlite3 (1.3.13) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + xcinvoke (0.3.0) + liferaft (~> 0.0.6) + xcodeproj (1.7.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.6) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods (~> 1.6.0.beta.2) + jazzy + +BUNDLED WITH + 1.17.1 diff --git a/Info.plist b/Info.plist index a02ba6c2..4b5fa2ec 100644 --- a/Info.plist +++ b/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.0.3 + 5.1.0 CFBundleSignature ???? CFBundleVersion diff --git a/OAuth2.xcodeproj/project.pbxproj b/OAuth2.xcodeproj/project.pbxproj index 36f54c31..a26f30b7 100644 --- a/OAuth2.xcodeproj/project.pbxproj +++ b/OAuth2.xcodeproj/project.pbxproj @@ -31,7 +31,7 @@ 65EC05E11C9050CB00DE9186 /* OAuth2KeychainAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65EC05DF1C9050CB00DE9186 /* OAuth2KeychainAccount.swift */; }; 65EC05E21C9050CB00DE9186 /* OAuth2KeychainAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65EC05DF1C9050CB00DE9186 /* OAuth2KeychainAccount.swift */; }; CCCE40D6B4EAD9BF05C92ACE /* OAuth2CustomAuthorizer+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCCE4C8DC3CB7713E59BC1EE /* OAuth2CustomAuthorizer+iOS.swift */; }; - DD0CCBAD1C4DC83A0044C4E3 /* OAuth2WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController.swift */; }; + DD0CCBAD1C4DC83A0044C4E3 /* OAuth2WebViewController+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController+macOS.swift */; }; EA9758181B222CEA007744B1 /* OAuth2PasswordGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9758171B222CEA007744B1 /* OAuth2PasswordGrant.swift */; }; EA97581E1B2242F9007744B1 /* OAuth2PasswordGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9758171B222CEA007744B1 /* OAuth2PasswordGrant.swift */; }; EE1070341E5C7A4200250586 /* OAuth2CustomAuthorizerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE1070331E5C7A4200250586 /* OAuth2CustomAuthorizerUI.swift */; }; @@ -42,7 +42,7 @@ EE20118C1E44D0BD00913FA7 /* OAuth2DataLoaderSessionTaskDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE20118B1E44D0BD00913FA7 /* OAuth2DataLoaderSessionTaskDelegate.swift */; }; EE20118D1E44D0BD00913FA7 /* OAuth2DataLoaderSessionTaskDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE20118B1E44D0BD00913FA7 /* OAuth2DataLoaderSessionTaskDelegate.swift */; }; EE20118E1E44D0BD00913FA7 /* OAuth2DataLoaderSessionTaskDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE20118B1E44D0BD00913FA7 /* OAuth2DataLoaderSessionTaskDelegate.swift */; }; - EE24862A1AC85DD4002B31AF /* OAuth2WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2486291AC85DD4002B31AF /* OAuth2WebViewController.swift */; }; + EE24862A1AC85DD4002B31AF /* OAuth2WebViewController+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2486291AC85DD4002B31AF /* OAuth2WebViewController+iOS.swift */; }; EE2983701D40B83600933CDD /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29836F1D40B83600933CDD /* OAuth2.swift */; }; EE2983711D40B83600933CDD /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29836F1D40B83600933CDD /* OAuth2.swift */; }; EE2983721D40B83600933CDD /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29836F1D40B83600933CDD /* OAuth2.swift */; }; @@ -168,14 +168,15 @@ 6598543F1C5B3B4000237D39 /* OAuth2Authorizer+tvOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "OAuth2Authorizer+tvOS.swift"; path = "Sources/tvOS/OAuth2Authorizer+tvOS.swift"; sourceTree = SOURCE_ROOT; }; 659854461C5B3BEA00237D39 /* OAuth2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OAuth2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65EC05DF1C9050CB00DE9186 /* OAuth2KeychainAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2KeychainAccount.swift; sourceTree = ""; }; + B4EE6D0922FF07D6004BC0D4 /* OAuth2Module.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Module.swift; sourceTree = ""; }; CCCE4C8DC3CB7713E59BC1EE /* OAuth2CustomAuthorizer+iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OAuth2CustomAuthorizer+iOS.swift"; sourceTree = ""; }; - DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2WebViewController.swift; sourceTree = ""; }; + DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController+macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OAuth2WebViewController+macOS.swift"; sourceTree = ""; }; EA9758171B222CEA007744B1 /* OAuth2PasswordGrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2PasswordGrant.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE01F96E1C58D5D6003AEA7E /* generate-docs.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "generate-docs.sh"; sourceTree = ""; }; EE1070331E5C7A4200250586 /* OAuth2CustomAuthorizerUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CustomAuthorizerUI.swift; sourceTree = ""; }; EE1391D91AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2CodeGrantBasicAuth.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE20118B1E44D0BD00913FA7 /* OAuth2DataLoaderSessionTaskDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2DataLoaderSessionTaskDelegate.swift; sourceTree = ""; }; - EE2486291AC85DD4002B31AF /* OAuth2WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2WebViewController.swift; sourceTree = ""; }; + EE2486291AC85DD4002B31AF /* OAuth2WebViewController+iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OAuth2WebViewController+iOS.swift"; sourceTree = ""; }; EE29836A1D3FC28000933CDD /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; EE29836F1D40B83600933CDD /* OAuth2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2.swift; sourceTree = ""; }; EE2983741D40BE7600933CDD /* OAuth2AuthorizerUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2AuthorizerUI.swift; sourceTree = ""; }; @@ -268,12 +269,20 @@ path = tvOS; sourceTree = ""; }; + B4EE6D0822FF07D6004BC0D4 /* OAuth */ = { + isa = PBXGroup; + children = ( + B4EE6D0922FF07D6004BC0D4 /* OAuth2Module.swift */, + ); + path = OAuth; + sourceTree = ""; + }; EE2486281AC85DD4002B31AF /* iOS */ = { isa = PBXGroup; children = ( EEC7A8C81AE47111008C30E7 /* OAuth2Authorizer+iOS.swift */, CCCE4C8DC3CB7713E59BC1EE /* OAuth2CustomAuthorizer+iOS.swift */, - EE2486291AC85DD4002B31AF /* OAuth2WebViewController.swift */, + EE2486291AC85DD4002B31AF /* OAuth2WebViewController+iOS.swift */, ); path = iOS; sourceTree = ""; @@ -367,7 +376,7 @@ children = ( EEC7A8C61AE46C33008C30E7 /* OAuth2Authorizer+macOS.swift */, 19C919DC1E51CC8000BFC834 /* OAuth2CustomAuthorizer+macOS.swift */, - DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController.swift */, + DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController+macOS.swift */, ); path = macOS; sourceTree = ""; @@ -395,6 +404,7 @@ EEDB8625193FAAE500C4EEA1 /* Products */, ); sourceTree = ""; + usesTabs = 1; }; EEDB8625193FAAE500C4EEA1 /* Products */ = { isa = PBXGroup; @@ -411,6 +421,7 @@ EEDB8626193FAAE500C4EEA1 /* OAuth2 */ = { isa = PBXGroup; children = ( + B4EE6D0822FF07D6004BC0D4 /* OAuth */, EE2983731D40BC8900933CDD /* Base */, EE79F65C1BFBDFFF00746243 /* Flows */, EE9EBF111D775A21003263FC /* DataLoader */, @@ -565,7 +576,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 1100; ORGANIZATIONNAME = "Pascal Pfiffner"; TargetAttributes = { 659854451C5B3BEA00237D39 = { @@ -573,21 +584,22 @@ }; EEDB8623193FAAE500C4EEA1 = { CreatedOnToolsVersion = 6.0; + LastSwiftMigration = 1130; }; EEE209461942772800736F1A = { CreatedOnToolsVersion = 6.0; - LastSwiftMigration = 0800; + LastSwiftMigration = 1100; }; EEE209A119427DFE00736F1A = { CreatedOnToolsVersion = 6.0; - LastSwiftMigration = 0900; + LastSwiftMigration = 1100; TestTargetID = EEE209461942772800736F1A; }; }; }; buildConfigurationList = EEDB861E193FAAE500C4EEA1 /* Build configuration list for PBXProject "OAuth2" */; compatibilityVersion = "Xcode 6.3"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -710,7 +722,7 @@ EEACE1E01A7E8FC5009BF3A7 /* OAuth2CodeGrantFacebook.swift in Sources */, EE1391DB1AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift in Sources */, EE507A391B1E15E000AE02E9 /* OAuth2DynReg.swift in Sources */, - EE24862A1AC85DD4002B31AF /* OAuth2WebViewController.swift in Sources */, + EE24862A1AC85DD4002B31AF /* OAuth2WebViewController+iOS.swift in Sources */, EE79F6581BFA945C00746243 /* OAuth2ClientConfig.swift in Sources */, EE79F65B1BFAA36900746243 /* OAuth2Error.swift in Sources */, CCCE40D6B4EAD9BF05C92ACE /* OAuth2CustomAuthorizer+iOS.swift in Sources */, @@ -726,7 +738,7 @@ EEAEF10B1CDBCF28001A1C6F /* OAuth2Logger.swift in Sources */, 65EC05E01C9050CB00DE9186 /* OAuth2KeychainAccount.swift in Sources */, 0C2F5E5B1DE2DB8500F621E0 /* OAuth2CodeGrantAzure.swift in Sources */, - DD0CCBAD1C4DC83A0044C4E3 /* OAuth2WebViewController.swift in Sources */, + DD0CCBAD1C4DC83A0044C4E3 /* OAuth2WebViewController+macOS.swift in Sources */, EE9EBF1B1D775F74003263FC /* OAuth2Securable.swift in Sources */, EE79F65A1BFAA36900746243 /* OAuth2Error.swift in Sources */, EEC49F311C9BF22400989A18 /* OAuth2AuthRequest.swift in Sources */, @@ -868,6 +880,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -876,12 +889,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -910,7 +925,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 10.11; METAL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -926,6 +941,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -934,12 +950,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -961,7 +979,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 10.11; METAL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_VERSION = 4.0; @@ -976,7 +994,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -990,7 +1008,7 @@ PRODUCT_NAME = OAuth2; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -998,7 +1016,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1012,7 +1030,7 @@ PRODUCT_NAME = OAuth2; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -1034,7 +1052,7 @@ PRODUCT_NAME = OAuth2; SDKROOT = macosx; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -1058,7 +1076,7 @@ SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -1077,7 +1095,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.github.p2.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -1098,7 +1116,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2iOS.xcscheme b/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2iOS.xcscheme index a3685ef9..b6233a37 100644 --- a/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2iOS.xcscheme +++ b/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2iOS.xcscheme @@ -1,6 +1,6 @@ + + + + @@ -40,23 +48,11 @@ - - - - - - - - + + + + @@ -54,23 +62,11 @@ - - - - - - - - + + + + @@ -40,23 +48,11 @@ - - - - - - - - ``` +Need to specify a separate refresh token URI? You can set the `refresh_uri` in the Settings Dictionary. If specified the library will refresh access tokens using the `refresh_uri` you specified, otherwise it will use the `token_uri`. + Need to debug? Use a `.debug` or even a `.trace` logger: ```swift @@ -368,7 +369,7 @@ The main configuration you'll use with `oauth2.authConfig` is whether or not to oauth2.authConfig.authorizeEmbedded = true -Similarly, if you want to take care of dismissing the login screen yourself: +Similarly, if you want to take care of dismissing the login screen yourself (not possible with the newer authorization sessions mentioned below): oauth2.authConfig.authorizeEmbeddedAutoDismiss = false @@ -394,6 +395,10 @@ Similar is how you specify custom HTTP headers: "headers": ["Accept": "application/json, text/plain"] Starting with version 2.0.1 on iOS 9, `SFSafariViewController` will be used for embedded authorization. +Starting after version 4.2, on iOS 11 (`SFAuthenticationSession`) and iOS 12 (`ASWebAuthenticationSession`), you can opt-in to these newer authorization session view controllers: + + oauth2.authConfig.ui.useAuthenticationSession = true + To revert to the old custom `OAuth2WebViewController`: oauth2.authConfig.ui.useSafariView = false @@ -402,6 +407,7 @@ To customize the _go back_ button when using `OAuth2WebViewController` on iOS 8 oauth2.authConfig.ui.backButton = <# UIBarButtonItem(...) #> +See below for settings about [the keychain](#keychain) and [PKCE](#pkce). Usage with Alamofire -------------------- @@ -448,12 +454,18 @@ dynreg.register(client: oauth2) { params, error in } ``` +PKCE +---- + +PKCE support is controlled by the `useProofKeyForCodeExchange` property, and the `use_pkce` key in the settings dictionary. +It is disabled by default. When enabled, a new code verifier string is generated for every authorization request. + Keychain -------- This framework can transparently use the iOS and macOS keychain. -It is controlled by the `useKeychain` property, which can be disabled during initialization with the "keychain" setting. +It is controlled by the `useKeychain` property, which can be disabled during initialization with the `keychain` settings dictionary key. Since this is **enabled by default**, if you do _not_ turn it off during initialization, the keychain will be queried for tokens and client credentials related to the authorization URL. If you turn it off _after_ initialization, the keychain will be queried for existing tokens, but new tokens will not be written to the keychain. @@ -463,21 +475,32 @@ If you have dynamically registered your client and want to start anew, you can c Ideally, access tokens get delivered with an "expires_in" parameter that tells you how long the token is valid. If it is missing the framework will still use those tokens if one is found in the keychain and not re-perform the OAuth dance. You will need to intercept 401s and re-authorize if an access token has expired but the framework has still pulled it from the keychain. -This behavior can be turned off by supplying "token_assume_unexpired": false in settings or setting `clientConfig.accessTokenAssumeUnexpired` to false. +This behavior can be turned off by supplying `token_assume_unexpired: false` in settings or setting `clientConfig.accessTokenAssumeUnexpired` to false. +These are the settings dictionary keys you can use for more control: + +- `keychain`: a bool on whether to use keychain or not, true by default +- `keychain_access_mode`: a string value for keychain kSecAttrAccessible attribute, "kSecAttrAccessibleWhenUnlocked" by default, you can change this to e.g. "kSecAttrAccessibleAfterFirstUnlock" if you need the tokens to be available when the phone is locked. +- `keychain_access_group`: a string value for keychain kSecAttrAccessGroup attribute, nil by default +- `keychain_account_for_client_credentials`: the name to use to identify client credentials in the keychain, "clientCredentials" by default +- `keychain_account_for_tokens`: the name to use to identify the tokens in the keychain, "currentTokens" by default Installation ------------ -You can use _git_, _Carthage_ and even _CocoaPods_ to install the framework. -The preferred way is to use _git_ directly or _Carthage_. +You can use the _Swift Package Manager_, _git_ or _Carthage_. +The preferred way is to use the _Swift Package Manager_. + +#### Swift Package Manager + +In Xcode 11 and newer, choose "File" from the Xcode Menu, then "Swift Packages" ยป "Add Package Dependency..." and paste the URL of this repo: `https://github.com/p2/OAuth2.git`. Pick a version and Xcode should do the rest. #### Carthage Installation via Carthage is easy enough: ```ruby -github "p2/OAuth2" ~> 3.0 +github "p2/OAuth2" ~> 4.2 ``` #### git @@ -502,28 +525,6 @@ These three steps are needed to: 2. Link the framework into your app 3. Embed the framework in your app when distributing -#### CocoaPods - -CocoaPods was nice back in the days for Obj-C and static libraries, but is overkill in the modern days of Swift and iOS frameworks. -You can however still use OAuth2 with Cocoapods. - -Add a `Podfile` that contains at least the following information to the root of your app project, then do `pod install`. -If you're unfamiliar with CocoaPods, read [using CocoaPods](http://guides.cocoapods.org/using/using-cocoapods.html). - -```ruby -platform :ios, '8.0' # or platform :osx, '10.9' -use_frameworks! -target `YourApp` do - pod 'p2.OAuth2', '~> 3.0' -end -``` - -If you want the bleeding edge, use this command for CocoaPods instead โ€“ note the `submodules` flag: without it the library will not compile. - -```ruby -pod 'p2.OAuth2', :git => 'https://github.com/p2/OAuth2', :submodules => true -``` - License ------- @@ -533,4 +534,3 @@ Since there is no `NOTICE` file there is nothing that you have to include in you [sample]: https://github.com/p2/OAuth2App - diff --git a/Sources/Base/OAuth2AuthConfig.swift b/Sources/Base/OAuth2AuthConfig.swift index f9d5b59b..feec39ed 100644 --- a/Sources/Base/OAuth2AuthConfig.swift +++ b/Sources/Base/OAuth2AuthConfig.swift @@ -43,6 +43,9 @@ public struct OAuth2AuthConfig { /// Starting with iOS 9, `SFSafariViewController` will be used for embedded authorization instead of our custom class. You can turn this off here. public var useSafariView = true + /// Starting with iOS 12, `ASWebAuthenticationSession` can be used for embedded authorization instead of our custom class. You can turn this on here. + public var useAuthenticationSession = false + #if os(iOS) /// By assigning your own style you can configure how the embedded authorization is presented. public var modalPresentationStyle = UIModalPresentationStyle.fullScreen @@ -60,11 +63,11 @@ public struct OAuth2AuthConfig { /// Whether to automatically dismiss the auto-presented authorization screen. public var authorizeEmbeddedAutoDismiss = true - + /// Context information for the authorization flow: /// - iOS: The parent view controller to present from /// - macOS: An NSWindow from which to present a modal sheet _or_ `nil` to present in a new window - public var authorizeContext: AnyObject? = nil + public weak var authorizeContext: AnyObject? = nil /// UI-specific configuration. public var ui = UI() diff --git a/Sources/Base/OAuth2Base.swift b/Sources/Base/OAuth2Base.swift index b3edc293..392a170e 100644 --- a/Sources/Base/OAuth2Base.swift +++ b/Sources/Base/OAuth2Base.swift @@ -19,6 +19,7 @@ // import Foundation +import CommonCrypto /** @@ -154,6 +155,7 @@ open class OAuth2Base: OAuth2Securable { - client_secret (String), usually only needed for code grant - authorize_uri (URL-String) - token_uri (URL-String), if omitted the authorize_uri will be used to obtain tokens + - refresh_uri (URL-String), if omitted the token_uri will be used to obtain tokens - redirect_uris (Array of URL-Strings) - scope (String) @@ -169,6 +171,7 @@ open class OAuth2Base: OAuth2Securable { - secret_in_body (Bool, false by default, forces the flow to use the request body for the client secret) - parameters ([String: String], custom request parameters to be added during authorization) - token_assume_unexpired (Bool, true by default, whether to use access tokens that do not come with an "expires_in" parameter) + - use_pkce (Bool, false by default) - verbose (Bool, false by default, applies to client logging) */ @@ -454,6 +457,7 @@ open class OAuth2Base: OAuth2Securable { */ open func assureRefreshTokenParamsAreValid(_ params: OAuth2JSON) throws { } + } @@ -465,6 +469,10 @@ open class OAuth2ContextStore { /// Currently used redirect_url. open var redirectURL: String? + /// Current code verifier used for PKCE + public internal(set) var codeVerifier: String? + public let codeChallengeMethod = "S256" + /// The current state. internal var _state = "" @@ -500,5 +508,37 @@ open class OAuth2ContextStore { func resetState() { _state = "" } + + // MARK: - PKCE + + /** + Generates a new code verifier string + */ + open func generateCodeVerifier() { + var buffer = [UInt8](repeating: 0, count: 32) + _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer) + codeVerifier = Data(buffer).base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + .trimmingCharacters(in: .whitespaces) + } + + + open func codeChallenge() -> String? { + guard let verifier = codeVerifier, let data = verifier.data(using: .utf8) else { return nil } + var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &buffer) + } + let hash = Data(buffer) + let challenge = hash.base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + .trimmingCharacters(in: .whitespaces) + return challenge + } + } diff --git a/Sources/Base/OAuth2ClientConfig.swift b/Sources/Base/OAuth2ClientConfig.swift index 668b0e68..83fadd16 100644 --- a/Sources/Base/OAuth2ClientConfig.swift +++ b/Sources/Base/OAuth2ClientConfig.swift @@ -28,6 +28,9 @@ open class OAuth2ClientConfig { /// The URL where we can exchange a code for a token. public final var tokenURL: URL? + + /// The URL where we can refresh an access token using a refresh token. + public final var refreshURL: URL? /// Where a logo/icon for the app can be found. public final var logoURL: URL? @@ -90,6 +93,12 @@ open class OAuth2ClientConfig { /// url. open var safariCancelWorkaround = false + /// Use Proof Key for Code Exchange (PKCE) + /// + /// See https://tools.ietf.org/html/rfc7636 + /// + open var useProofKeyForCodeExchange = false + /** Initializer to initialize properties from a settings dictionary. */ @@ -109,6 +118,9 @@ open class OAuth2ClientConfig { if let token = settings["token_uri"] as? String { tokenURL = URL(string: token) } + if let refresh = settings["refresh_uri"] as? String { + refreshURL = URL(string: refresh) + } if let registration = settings["registration_uri"] as? String { registrationURL = URL(string: registration) } @@ -144,6 +156,11 @@ open class OAuth2ClientConfig { if let assume = settings["token_assume_unexpired"] as? Bool { accessTokenAssumeUnexpired = assume } + + if let usePKCE = settings["use_pkce"] as? Bool { + useProofKeyForCodeExchange = usePKCE + } + } diff --git a/Sources/Base/OAuth2Error.swift b/Sources/Base/OAuth2Error.swift index 50bed17f..161eeb49 100644 --- a/Sources/Base/OAuth2Error.swift +++ b/Sources/Base/OAuth2Error.swift @@ -338,7 +338,7 @@ public extension Error { /** Convenience getter to easily retrieve an OAuth2Error from any Error. */ - public var asOAuth2Error: OAuth2Error { + var asOAuth2Error: OAuth2Error { if let oaerror = self as? OAuth2Error { return oaerror } diff --git a/Sources/Base/OAuth2Logger.swift b/Sources/Base/OAuth2Logger.swift index 5383ffe4..c9ad436c 100644 --- a/Sources/Base/OAuth2Logger.swift +++ b/Sources/Base/OAuth2Logger.swift @@ -99,17 +99,17 @@ extension OAuth2Logger { /** Log a message at the trace level. */ public func trace(_ module: String? = "OAuth2", filename: String? = #file, line: Int? = #line, function: String? = #function, msg: @autoclosure() -> String) { - log(.trace, module: module, filename: filename, line: line, function: function, msg: msg) + log(.trace, module: module, filename: filename, line: line, function: function, msg: msg()) } /** Standard debug logging. */ public func debug(_ module: String? = "OAuth2", filename: String? = #file, line: Int? = #line, function: String? = #function, msg: @autoclosure() -> String) { - log(.debug, module: module, filename: filename, line: line, function: function, msg: msg) + log(.debug, module: module, filename: filename, line: line, function: function, msg: msg()) } /** Log warning messages. */ public func warn(_ module: String? = "OAuth2", filename: String? = #file, line: Int? = #line, function: String? = #function, msg: @autoclosure() -> String) { - log(.warn, module: module, filename: filename, line: line, function: function, msg: msg) + log(.warn, module: module, filename: filename, line: line, function: function, msg: msg()) } } diff --git a/Sources/Flows/OAuth2.swift b/Sources/Flows/OAuth2.swift index 025ae988..c5976c14 100644 --- a/Sources/Flows/OAuth2.swift +++ b/Sources/Flows/OAuth2.swift @@ -57,6 +57,7 @@ open class OAuth2: OAuth2Base { - client_secret (String), usually only needed for code grant - authorize_uri (URL-String) - token_uri (URL-String), if omitted the authorize_uri will be used to obtain tokens + - refresh_uri (URL-String), if omitted the token_uri will be used to obtain tokens - redirect_uris (Array of URL-Strings) - scope (String) @@ -72,6 +73,7 @@ open class OAuth2: OAuth2Base { - secret_in_body (Bool, false by default, forces the flow to use the request body for the client secret) - parameters ([String: String], custom request parameters to be added during authorization) - token_assume_unexpired (Bool, true by default, whether to use access tokens that do not come with an "expires_in" parameter) + - use_pkce (Bool, false by default) - verbose (bool, false by default, applies to client logging) */ @@ -285,6 +287,11 @@ open class OAuth2: OAuth2Base { if clientConfig.safariCancelWorkaround { req.params["swa"] = "\(Date.timeIntervalSinceReferenceDate)" // Safari issue workaround } + if clientConfig.useProofKeyForCodeExchange { + context.generateCodeVerifier() + req.params["code_challenge"] = context.codeChallenge() + req.params["code_challenge_method"] = context.codeChallengeMethod + } req.add(params: params) return req @@ -338,7 +345,7 @@ open class OAuth2: OAuth2Base { throw OAuth2Error.noRefreshToken } - let req = OAuth2AuthRequest(url: (clientConfig.tokenURL ?? clientConfig.authorizeURL)) + let req = OAuth2AuthRequest(url: (clientConfig.refreshURL ?? clientConfig.tokenURL ?? clientConfig.authorizeURL)) req.params["grant_type"] = "refresh_token" req.params["refresh_token"] = refreshToken if let clientId = clientId { @@ -396,7 +403,7 @@ open class OAuth2: OAuth2Base { - parameter callback: The callback to call on the main thread; if both json and error is nil no registration was attempted; error is nil on success */ - func registerClientIfNeeded(callback: @escaping ((OAuth2JSON?, OAuth2Error?) -> Void)) { + public func registerClientIfNeeded(callback: @escaping ((OAuth2JSON?, OAuth2Error?) -> Void)) { if nil != clientId || !type(of: self).clientIdMandatory { callOnMainThread() { callback(nil, nil) diff --git a/Sources/Flows/OAuth2CodeGrant.swift b/Sources/Flows/OAuth2CodeGrant.swift index 7896d389..f97a40a3 100644 --- a/Sources/Flows/OAuth2CodeGrant.swift +++ b/Sources/Flows/OAuth2CodeGrant.swift @@ -67,7 +67,9 @@ open class OAuth2CodeGrant: OAuth2 { req.params["grant_type"] = type(of: self).grantType req.params["redirect_uri"] = redirect req.params["client_id"] = clientId - + if clientConfig.useProofKeyForCodeExchange { + req.params["code_verifier"] = context.codeVerifier + } return req } diff --git a/Sources/OAuth2/OAuth2Module.swift b/Sources/OAuth2/OAuth2Module.swift new file mode 100644 index 00000000..2c465b8f --- /dev/null +++ b/Sources/OAuth2/OAuth2Module.swift @@ -0,0 +1,13 @@ +// +// OAuth2.swift +// OAuth2 +// +// Created by Dave Carlson on 8/9/19. +// + +@_exported import Base +@_exported import macOS +@_exported import iOS +@_exported import tvOS +@_exported import Flows +@_exported import DataLoader diff --git a/Sources/iOS/OAuth2Authorizer+iOS.swift b/Sources/iOS/OAuth2Authorizer+iOS.swift index 6fe1705b..1fcd16c2 100644 --- a/Sources/iOS/OAuth2Authorizer+iOS.swift +++ b/Sources/iOS/OAuth2Authorizer+iOS.swift @@ -21,6 +21,7 @@ import UIKit import SafariServices +import AuthenticationServices #if !NO_MODULE_IMPORT import Base #endif @@ -39,8 +40,10 @@ open class OAuth2Authorizer: OAuth2AuthorizerUI { /// Used to store the `SFSafariViewControllerDelegate`. private var safariViewDelegate: AnyObject? + /// Used to store the authentication session. + private var authenticationSession: AnyObject? - public init(oauth2: OAuth2) { + public init(oauth2: OAuth2Base) { self.oauth2 = oauth2 } @@ -55,17 +58,13 @@ open class OAuth2Authorizer: OAuth2AuthorizerUI { */ public func openAuthorizeURLInBrowser(_ url: URL) throws { - // By asking for the shared instance method by using the "value for key" method on UIApplication, we are able to - // bypass the Swift compilation restriction that blocks the library from being compiled for an extension when - // directly referencing it. We do it as an optional so in the advent of this method being called, like in an - // extension, we handle it as though its not supported. - guard let application = UIApplication.value(forKey: "sharedApplication") as? UIApplication else { - throw OAuth2Error.unableToOpenAuthorizeURL - } - - if !application.openURL(url) { + #if !P2_APP_EXTENSIONS + if !UIApplication.shared.openURL(url) { throw OAuth2Error.unableToOpenAuthorizeURL } + #else + throw OAuth2Error.unableToOpenAuthorizeURL + #endif } /** @@ -76,23 +75,31 @@ open class OAuth2Authorizer: OAuth2AuthorizerUI { - parameter at: The authorize URL to open */ public func authorizeEmbedded(with config: OAuth2AuthConfig, at url: URL) throws { - guard let controller = config.authorizeContext as? UIViewController else { - throw (nil == config.authorizeContext) ? OAuth2Error.noAuthorizationContext : OAuth2Error.invalidAuthorizationContext - } - - if #available(iOS 9, *), config.ui.useSafariView { - let web = try authorizeSafariEmbedded(from: controller, at: url) - if config.authorizeEmbeddedAutoDismiss { - oauth2.internalAfterAuthorizeOrFail = { wasFailure, error in - web.dismiss(animated: true) + if #available(iOS 11, *), config.ui.useAuthenticationSession { + guard let redirect = oauth2.redirect else { + throw OAuth2Error.noRedirectURL + } + + authenticationSessionEmbedded(at: url, withRedirect: redirect) + } else { + guard let controller = config.authorizeContext as? UIViewController else { + throw (nil == config.authorizeContext) ? OAuth2Error.noAuthorizationContext : OAuth2Error.invalidAuthorizationContext + } + + if #available(iOS 9, *), config.ui.useSafariView { + let web = try authorizeSafariEmbedded(from: controller, at: url) + if config.authorizeEmbeddedAutoDismiss { + oauth2.internalAfterAuthorizeOrFail = { wasFailure, error in + web.dismiss(animated: true) + } } } - } - else { - let web = try authorizeEmbedded(from: controller, at: url) - if config.authorizeEmbeddedAutoDismiss { - oauth2.internalAfterAuthorizeOrFail = { wasFailure, error in - web.dismiss(animated: true) + else { + let web = try authorizeEmbedded(from: controller, at: url) + if config.authorizeEmbeddedAutoDismiss { + oauth2.internalAfterAuthorizeOrFail = { wasFailure, error in + web.dismiss(animated: true) + } } } } @@ -108,6 +115,52 @@ open class OAuth2Authorizer: OAuth2AuthorizerUI { open func willPresent(viewController: UIViewController, in naviController: UINavigationController?) { } + // MARK: - SFAuthenticationSession / ASWebAuthenticationSession + + /** + Use SFAuthenticationSession or ASWebAuthenticationSession to manage authorisation. + + On iOS 11, use SFAuthenticationSession. On iOS 12+, use ASWebAuthenticationSession. + + The mechanism works just like when you're using Safari itself to log the user in, hence you **need to implement** + `application(application:openURL:sourceApplication:annotation:)` in your application delegate. + + This method dismisses the view controller automatically - this cannot be disabled. + + - parameter at: The authorize URL to open + - returns: A Boolean value indicating whether the web authentication session starts successfully. + */ + @available(iOS 11.0, *) + @discardableResult + public func authenticationSessionEmbedded(at url: URL, withRedirect redirect: String) -> Bool { + let completionHandler: (URL?, Error?) -> Void = { url, error in + if let url = url { + do { + try self.oauth2.handleRedirectURL(url as URL) + } + catch let err { + self.oauth2.logger?.warn("OAuth2", msg: "Cannot intercept redirect URL: \(err)") + } + } else { + self.oauth2.didFail(with: nil) + } + self.authenticationSession = nil + } + +#if targetEnvironment(macCatalyst) + authenticationSession = ASWebAuthenticationSession(url: url, callbackURLScheme: redirect, completionHandler: completionHandler) + return (authenticationSession as! ASWebAuthenticationSession).start() +#else + if #available(iOS 12, *) { + authenticationSession = ASWebAuthenticationSession(url: url, callbackURLScheme: redirect, completionHandler: completionHandler) + return (authenticationSession as! ASWebAuthenticationSession).start() + } else { + authenticationSession = SFAuthenticationSession(url: url, callbackURLScheme: redirect, completionHandler: completionHandler) + return (authenticationSession as! SFAuthenticationSession).start() + } +#endif + } + // MARK: - Safari Web View Controller @@ -139,13 +192,14 @@ open class OAuth2Authorizer: OAuth2AuthorizerUI { web.preferredControlTintColor = tint } web.modalPresentationStyle = oauth2.authConfig.ui.modalPresentationStyle - + willPresent(viewController: web, in: nil) controller.present(web, animated: true, completion: nil) return web } + /** Called from our delegate, which reacts to users pressing "Done". We can assume this is always a cancel as nomally the Safari view controller is dismissed automatically. diff --git a/Sources/iOS/OAuth2WebViewController.swift b/Sources/iOS/OAuth2WebViewController+iOS.swift similarity index 99% rename from Sources/iOS/OAuth2WebViewController.swift rename to Sources/iOS/OAuth2WebViewController+iOS.swift index 0867c04c..ee8eb12c 100644 --- a/Sources/iOS/OAuth2WebViewController.swift +++ b/Sources/iOS/OAuth2WebViewController+iOS.swift @@ -32,7 +32,7 @@ A simple iOS web view controller that allows you to display the login/authorizat open class OAuth2WebViewController: UIViewController, WKNavigationDelegate { /// Handle to the OAuth2 instance in play, only used for debug lugging at this time. - var oauth: OAuth2? + var oauth: OAuth2Base? /// The URL to load on first show. open var startURL: URL? { diff --git a/Sources/macOS/OAuth2WebViewController.swift b/Sources/macOS/OAuth2WebViewController+macOS.swift similarity index 86% rename from Sources/macOS/OAuth2WebViewController.swift rename to Sources/macOS/OAuth2WebViewController+macOS.swift index 47347621..3eef94f4 100644 --- a/Sources/macOS/OAuth2WebViewController.swift +++ b/Sources/macOS/OAuth2WebViewController+macOS.swift @@ -88,15 +88,16 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS let view = NSView(frame: self.view.bounds) view.translatesAutoresizingMaskIntoConstraints = false - progressIndicator = NSProgressIndicator(frame: NSZeroRect) - progressIndicator.style = .spinning - progressIndicator.isDisplayedWhenStopped = false - progressIndicator.sizeToFit() - progressIndicator.translatesAutoresizingMaskIntoConstraints = false + let indicator = NSProgressIndicator(frame: NSZeroRect) + indicator.style = .spinning + indicator.isDisplayedWhenStopped = false + indicator.sizeToFit() + indicator.translatesAutoresizingMaskIntoConstraints = false + progressIndicator = indicator - view.addSubview(progressIndicator) - view.addConstraint(NSLayoutConstraint(item: progressIndicator, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0)) - view.addConstraint(NSLayoutConstraint(item: progressIndicator, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 0.0)) + view.addSubview(indicator) + view.addConstraint(NSLayoutConstraint(item: indicator, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0)) + view.addConstraint(NSLayoutConstraint(item: indicator, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 0.0)) return view } @@ -120,16 +121,17 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS view = NSView(frame: NSMakeRect(0, 0, OAuth2WebViewController.webViewWindowWidth, OAuth2WebViewController.webViewWindowHeight)) view.translatesAutoresizingMaskIntoConstraints = false - webView = WKWebView(frame: view.bounds, configuration: WKWebViewConfiguration()) - webView.translatesAutoresizingMaskIntoConstraints = false - webView.navigationDelegate = self - webView.alphaValue = 0.0 + let web = WKWebView(frame: view.bounds, configuration: WKWebViewConfiguration()) + web.translatesAutoresizingMaskIntoConstraints = false + web.navigationDelegate = self + web.alphaValue = 0.0 + webView = web - view.addSubview(webView) - view.addConstraint(NSLayoutConstraint(item: webView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 0.0)) - view.addConstraint(NSLayoutConstraint(item: webView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.0, constant: (willBecomeSheet ? -40.0 : 0.0))) - view.addConstraint(NSLayoutConstraint(item: webView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 0.0)) - view.addConstraint(NSLayoutConstraint(item: webView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: 0.0)) + view.addSubview(web) + view.addConstraint(NSLayoutConstraint(item: web, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 0.0)) + view.addConstraint(NSLayoutConstraint(item: web, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.0, constant: (willBecomeSheet ? -40.0 : 0.0))) + view.addConstraint(NSLayoutConstraint(item: web, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 0.0)) + view.addConstraint(NSLayoutConstraint(item: web, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: 0.0)) // add a dismiss button if willBecomeSheet { diff --git a/Tests/BaseTests/OAuth2Tests.swift b/Tests/BaseTests/OAuth2Tests.swift index be262241..57e60ecf 100644 --- a/Tests/BaseTests/OAuth2Tests.swift +++ b/Tests/BaseTests/OAuth2Tests.swift @@ -43,6 +43,18 @@ class OAuth2Tests: XCTestCase { "keychain": false, ]) } + + func refreshOAuth2() -> OAuth2 { + return OAuth2(settings: [ + "client_id": "abc", + "authorize_uri": "https://auth.ful.io", + "token_uri": "https://token.ful.io", + "refresh_uri": "https://refresh.ful.io", + "scope": "login", + "verbose": true, + "keychain": false, + ]) + } func testInit() { var oauth = OAuth2(settings: ["client_id": "def"]) @@ -88,6 +100,22 @@ class OAuth2Tests: XCTestCase { //XCTAssertEqual(params["redirect_uri"]!, "oauth2app://callback", "Expecting correct `redirect_uri` in query") XCTAssertNil(params["state"], "Expecting no `state` in query") } + + func testTokenRefreshRequest() { + let oa = refreshOAuth2() + oa.verbose = false + oa.clientConfig.refreshToken = "abc" + let req = try! oa.tokenRequestForTokenRefresh().asURLRequest(for: oa) + let auth = req.url! + + let comp = URLComponents(url: auth, resolvingAgainstBaseURL: true)! + XCTAssertEqual("https", comp.scheme!, "Need correct scheme") + XCTAssertEqual("refresh.ful.io", comp.host!, "Need correct host") + + let params = OAuth2.params(fromQuery: comp.percentEncodedQuery ?? "") + //XCTAssertEqual(params["redirect_uri"]!, "oauth2app://callback", "Expecting correct `redirect_uri` in query") + XCTAssertNil(params["state"], "Expecting no `state` in query") + } func testAuthorizeCall() { let oa = genericOAuth2() diff --git a/Tests/FlowTests/OAuth2CodeGrantTests.swift b/Tests/FlowTests/OAuth2CodeGrantTests.swift index 7fd298ca..d386e7e1 100644 --- a/Tests/FlowTests/OAuth2CodeGrantTests.swift +++ b/Tests/FlowTests/OAuth2CodeGrantTests.swift @@ -97,6 +97,26 @@ class OAuth2CodeGrantTests: XCTestCase { XCTAssertTrue(8 == (query["state"]!).count, "Expecting an auto-generated UUID for `state`") } + func testAuthorizeURIWithPKCE() { + let oauth = OAuth2CodeGrant(settings: [ + "client_id": "abc", + "authorize_uri": "https://auth.ful.io", + "token_uri": "https://token.ful.io", + "keychain": false, + "use_pkce" : true, + ]) + XCTAssertNotNil(oauth.authURL, "Must init `authorize_uri`") + + let comp = URLComponents(url: try! oauth.authorizeURL(withRedirect: "oauth2://callback", scope: nil, params: nil), resolvingAgainstBaseURL: true)! + XCTAssertEqual(comp.host!, "auth.ful.io", "Correct host") + let query = OAuth2CodeGrant.params(fromQuery: comp.percentEncodedQuery!) + XCTAssertEqual(query["client_id"]!, "abc", "Expecting correct `client_id`") + XCTAssertNotNil(query["code_challenge"], "Must have `code_challenge`") + XCTAssertEqual(query["code_challenge_method"]!, "S256", "Expecting correct `code_challenge_method`") + XCTAssertEqual(query["redirect_uri"]!, "oauth2://callback", "Expecting correct `redirect_uri`") + XCTAssertTrue(8 == (query["state"]!).count, "Expecting an auto-generated UUID for `state`") + } + func testRedirectURI() { let oauth = OAuth2CodeGrant(settings: baseSettings) oauth.redirect = "oauth2://callback" @@ -275,7 +295,50 @@ class OAuth2CodeGrantTests: XCTestCase { XCTAssertEqual(query2["redirect_uri"]!, "oauth2://callback", "Expecting correct `redirect_uri`") XCTAssertNil(query2["state"], "`state` must be empty") } - + + func testTokenRequestWithPKCE() { + let oauth = OAuth2CodeGrant(settings: [ + "client_id": "abc", + "authorize_uri": "https://auth.ful.io", + "token_uri": "https://token.ful.io", + "keychain": false, + "use_pkce" : true, + ]) + oauth.redirect = "oauth2://callback" + + // no redirect in context - fail + do { + _ = try oauth.accessTokenRequest(with: "pp") + XCTAssertTrue(false, "Should not be here any more") + } + catch OAuth2Error.noRedirectURL { + XCTAssertTrue(true, "Must be here") + } + catch { + XCTAssertTrue(false, "Should not be here") + } + + // with redirect in context - success + oauth.context.redirectURL = "oauth2://callback" + + // initialize code verifier in context + oauth.context.generateCodeVerifier() + + let req = try! oauth.accessTokenRequest(with: "pp").asURLRequest(for: oauth) + let comp = URLComponents(url: req.url!, resolvingAgainstBaseURL: true)! + XCTAssertEqual(comp.host!, "token.ful.io", "Correct host") + + let body = String(data: req.httpBody!, encoding: String.Encoding.utf8) + let query = OAuth2CodeGrant.params(fromQuery: body!) + XCTAssertEqual(query["client_id"]!, "abc", "Expecting correct `client_id`") + XCTAssertNil(query["client_secret"], "Must not have `client_secret`") + XCTAssertEqual(query["code"]!, "pp", "Expecting correct `code`") + XCTAssertEqual(query["grant_type"]!, "authorization_code", "Expecting correct `grant_type`") + XCTAssertEqual(query["redirect_uri"]!, "oauth2://callback", "Expecting correct `redirect_uri`") + XCTAssertNil(query["state"], "`state` must be empty") + XCTAssertNotNil(query["code_verifier"], "Must have `code_verifier`") + } + func testCustomAuthParameters() { let oauth = OAuth2CodeGrant(settings: baseSettings) oauth.redirect = "oauth2://callback" diff --git a/Tests/FlowTests/OAuth2RefreshTokenTests.swift b/Tests/FlowTests/OAuth2RefreshTokenTests.swift index 05ea4852..e106e5a2 100644 --- a/Tests/FlowTests/OAuth2RefreshTokenTests.swift +++ b/Tests/FlowTests/OAuth2RefreshTokenTests.swift @@ -41,6 +41,16 @@ class OAuth2RefreshTokenTests: XCTestCase { "keychain": false, ]) } + + func refreshOAuth2() -> OAuth2 { + return OAuth2(settings: [ + "client_id": "abc", + "authorize_uri": "https://auth.ful.io", + "token_uri": "https://token.ful.io", + "refresh_uri": "https://refresh.ful.io", + "keychain": false, + ]) + } func testCannotRefresh() { let oauth = genericOAuth2() @@ -76,6 +86,28 @@ class OAuth2RefreshTokenTests: XCTestCase { XCTAssertNil(dict["client_secret"]) XCTAssertNil(req!.allHTTPHeaderFields?["Authorization"]) } + + func testRefreshRequestWithDedicatedRefreshURI() { + let oauth = refreshOAuth2() + oauth.clientConfig.refreshToken = "pov" + + let req = try? oauth.tokenRequestForTokenRefresh().asURLRequest(for: oauth) + XCTAssertNotNil(req) + XCTAssertNotNil(req?.url) + XCTAssertNotNil(req?.httpBody) + XCTAssertEqual("https://refresh.ful.io", req!.url!.absoluteString) + let comp = URLComponents(url: req!.url!, resolvingAgainstBaseURL: true) + let params = comp?.percentEncodedQuery + XCTAssertNil(params) + let body = String(data: req!.httpBody!, encoding: String.Encoding.utf8) + XCTAssertNotNil(body) + let dict = OAuth2.params(fromQuery: body!) + XCTAssertEqual(dict["client_id"], "abc") + XCTAssertEqual(dict["refresh_token"], "pov") + XCTAssertEqual(dict["grant_type"], "refresh_token") + XCTAssertNil(dict["client_secret"]) + XCTAssertNil(req!.allHTTPHeaderFields?["Authorization"]) + } func testRefreshRequestWithSecret() { let oauth = genericOAuth2() diff --git a/generate-docs.sh b/generate-docs.sh index b5ead1e1..d15b92f9 100755 --- a/generate-docs.sh +++ b/generate-docs.sh @@ -1,12 +1,12 @@ #!/bin/bash # # Build documentation using jazzy: -# [sudo] gem install jazzy +# [sudo] bundle install -jazzy \ +bundle exec jazzy \ -o "docs" \ --min-acl "internal" \ - --module-version "3.0.3" + --module-version "5.0.0" mkdir docs/assets 2>/dev/null cp assets/* docs/assets/ diff --git a/p2.OAuth2.podspec b/p2.OAuth2.podspec index 538b6705..307ddac7 100644 --- a/p2.OAuth2.podspec +++ b/p2.OAuth2.podspec @@ -6,9 +6,9 @@ # Pod::Spec.new do |s| - s.name = "p2.OAuth2" - s.version = "4.0.1" - s.summary = "OAuth2 framework for macOS, iOS and tvOS, written in Swift." + s.name = 'p2.OAuth2' + s.version = '5.1.0' + s.summary = 'OAuth2 framework for macOS, iOS and tvOS, written in Swift.' s.description = <<-DESC OAuth2 frameworks for macOS, iOS and tvOS written in Swift. @@ -19,21 +19,37 @@ Pod::Spec.new do |s| Start with `import p2_OAuth2` in your source files. Code documentation is available from within Xcode (ALT + click on symbols) and on [p2.github.io/OAuth2/](http://p2.github.io/OAuth2/). DESC - s.homepage = "https://github.com/p2/OAuth2" - s.documentation_url = "http://p2.github.io/OAuth2/" - s.license = "Apache 2" - s.author = { "Pascal Pfiffner" => "phase.of.matter@gmail.com" } - s.source = { :git => "https://github.com/p2/OAuth2.git", :tag => "#{s.version}", :submodules => true } + s.homepage = 'https://github.com/p2/OAuth2' + s.documentation_url = 'http://p2.github.io/OAuth2/' + s.license = 'Apache 2' + s.author = { + 'Pascal Pfiffner' => 'phase.of.matter@gmail.com' + } - s.ios.deployment_target = "8.0" - s.osx.deployment_target = "10.10" - s.tvos.deployment_target = "9.0" - s.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS" => "-DNO_MODULE_IMPORT -DNO_KEYCHAIN_IMPORT" } + s.source = { + :git => 'https://github.com/p2/OAuth2.git', + :tag => s.version.to_s, + :submodules => true + } + s.swift_version = '5.0' + s.cocoapods_version = '>= 1.4.0' - s.source_files = "SwiftKeychain/Keychain/*.swift", "Sources/Base/*.swift", "Sources/Flows/*.swift", "Sources/DataLoader/*.swift" - s.ios.source_files = "Sources/iOS/*.swift" - s.osx.source_files = "Sources/macOS/*.swift" - s.tvos.source_files = "Sources/tvOS/*.swift" + s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.11' + s.tvos.deployment_target = '9.0' + s.pod_target_xcconfig = { + 'OTHER_SWIFT_FLAGS' => '-DNO_MODULE_IMPORT -DNO_KEYCHAIN_IMPORT' + } - s.ios.framework = "SafariServices" + s.source_files = [ + 'SwiftKeychain/Keychain/*.swift', + 'Sources/Base/*.swift', + 'Sources/Flows/*.swift', + 'Sources/DataLoader/*.swift' + ] + s.ios.source_files = 'Sources/iOS/*.swift' + s.osx.source_files = 'Sources/macOS/*.swift' + s.tvos.source_files = 'Sources/tvOS/*.swift' + + s.ios.framework = 'SafariServices' end