diff --git a/.jazzy.yaml b/.jazzy.yaml index 19c91053..3fe580d5 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -1,4 +1,9 @@ # Jazzy config +module: OAuth2 + author_url: http://www.github.com/p2 root_url: http://smart-on-fhir.github.io/Swift-SMART github_url: http://p2.github.io/OAuth2 + +theme: fullwidth +readme: README.md diff --git a/.travis.yml b/.travis.yml index d7409f83..8c14e681 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,6 @@ language: objective-c # well "swift" actually osx_image: xcode8 xcode_project: OAuth2.xcodeproj xcode_scheme: OAuth2OSX -xcode_sdk: macosx10.11 +xcode_sdk: macosx script: - travis_wait xcodebuild diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a808768..effb0d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ Version numbering represents the Swift version, plus a running number representi You can also refer to commit logs to get details on what was implemented, fixed and improved. +### 3.0.0 + +- Rewrite in Swift 3 +- New DataLoader, meaning you don't have to do authorization yourself (and helps with Alamofire use) +- Broad API redesign, you should now use `authorize(params:callback:)` if you still authorize manually +- All errors returned by OAuth2 are now `OAuth2Error` types +- Add `Package.swift` for the Swift package manager +- Expose `keychainAccessGroup` (`keychain_access_group` in settings; thanks @damienrambout !) +- Some new errors (like `.forbidden` and `.missingState`) + + ### 2.3.0 - Use Swift 2.3 @@ -12,7 +23,7 @@ You can also refer to commit logs to get details on what was implemented, fixed ### 2.2.9 -- Allow to add custom authentication headers (thanks @SpectralDragon) +- Allow to add custom authorization headers (thanks @SpectralDragon) - Fix: add `client_id` to password grant even if there is no secret (thanks Criss!) @@ -49,7 +60,7 @@ You can also refer to commit logs to get details on what was implemented, fixed ### 2.2.3 -- Refactor authentication request creation +- Refactor authorization request creation - Add `OAuth2ClientCredentialsReddit` to deal with Reddit installed apps special flow - Rename clashing method definitions to fix #99 diff --git a/Info.plist b/Info.plist index 97552b28..1d9dbbe0 100644 --- a/Info.plist +++ b/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.3.0 + 3.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/OAuth2.xcodeproj/project.pbxproj b/OAuth2.xcodeproj/project.pbxproj index e6b61842..57766cf3 100644 --- a/OAuth2.xcodeproj/project.pbxproj +++ b/OAuth2.xcodeproj/project.pbxproj @@ -7,9 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 6598544E1C5B3C9500237D39 /* OAuth2+tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6598543F1C5B3B4000237D39 /* OAuth2+tvOS.swift */; }; - 6598544F1C5B3C9C00237D39 /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB8640193FAB9200C4EEA1 /* OAuth2.swift */; }; - 659854501C5B3C9C00237D39 /* OAuth2Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF47D2A1B1E3FDD0057D838 /* OAuth2Base.swift */; }; + 6598544E1C5B3C9500237D39 /* OAuth2Authorizer+tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6598543F1C5B3B4000237D39 /* OAuth2Authorizer+tvOS.swift */; }; + 6598544F1C5B3C9C00237D39 /* OAuth2Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB8640193FAB9200C4EEA1 /* OAuth2Base.swift */; }; + 659854501C5B3C9C00237D39 /* OAuth2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF47D2A1B1E3FDD0057D838 /* OAuth2Requestable.swift */; }; 659854511C5B3C9C00237D39 /* OAuth2ClientConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE79F6561BFA945C00746243 /* OAuth2ClientConfig.swift */; }; 659854521C5B3C9C00237D39 /* OAuth2AuthConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE79F6531BFA93D900746243 /* OAuth2AuthConfig.swift */; }; 659854531C5B3CA700237D39 /* OAuth2ImplicitGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3174EB1945E83100210E62 /* OAuth2ImplicitGrant.swift */; }; @@ -21,7 +21,7 @@ 659854591C5B3CA700237D39 /* OAuth2ClientCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5F4F851B19114D00DB38B3 /* OAuth2ClientCredentials.swift */; }; 6598545A1C5B3CA700237D39 /* OAuth2PasswordGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9758171B222CEA007744B1 /* OAuth2PasswordGrant.swift */; }; 6598545B1C5B3CAB00237D39 /* OAuth2DynReg.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE507A371B1E15E000AE02E9 /* OAuth2DynReg.swift */; }; - 6598545C1C5B3CAB00237D39 /* OAuth2Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29EABB195A0DB2008882C8 /* OAuth2Request.swift */; }; + 6598545C1C5B3CAB00237D39 /* OAuth2DebugURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29EABB195A0DB2008882C8 /* OAuth2DebugURLSessionDelegate.swift */; }; 6598545D1C5B3CAB00237D39 /* OAuth2Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE79F6591BFAA36900746243 /* OAuth2Error.swift */; }; 6598545E1C5B3CAB00237D39 /* extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB864B19421DAD00C4EEA1 /* extensions.swift */; }; 659854611C5B3CB900237D39 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7A8D01AE4851E008C30E7 /* Keychain.swift */; }; @@ -30,18 +30,28 @@ 65EC05E21C9050CB00DE9186 /* OAuth2KeychainAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65EC05DF1C9050CB00DE9186 /* OAuth2KeychainAccount.swift */; }; DD0CCBAD1C4DC83A0044C4E3 /* OAuth2WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController.swift */; }; EA9758181B222CEA007744B1 /* OAuth2PasswordGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9758171B222CEA007744B1 /* OAuth2PasswordGrant.swift */; }; - EA97581D1B223A5D007744B1 /* OAuth2PasswordGrant_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA97581B1B223932007744B1 /* OAuth2PasswordGrant_tests.swift */; }; EA97581E1B2242F9007744B1 /* OAuth2PasswordGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9758171B222CEA007744B1 /* OAuth2PasswordGrant.swift */; }; EE1391DA1AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE1391D91AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift */; }; EE1391DB1AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE1391D91AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift */; }; EE24862A1AC85DD4002B31AF /* OAuth2WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2486291AC85DD4002B31AF /* OAuth2WebViewController.swift */; }; - EE414B6D1C0F6BDE00DE6A8F /* OAuth2DynReg_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE414B6C1C0F6BDE00DE6A8F /* OAuth2DynReg_tests.swift */; }; - EE43B15F1952E4700017679A /* OAuth2CodeGrant_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE43B1591951FAC70017679A /* OAuth2CodeGrant_tests.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 */; }; + EE2983751D40BE7600933CDD /* OAuth2AuthorizerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2983741D40BE7600933CDD /* OAuth2AuthorizerUI.swift */; }; + EE2983761D40BE7600933CDD /* OAuth2AuthorizerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2983741D40BE7600933CDD /* OAuth2AuthorizerUI.swift */; }; + EE2983771D40BE7600933CDD /* OAuth2AuthorizerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2983741D40BE7600933CDD /* OAuth2AuthorizerUI.swift */; }; + EE4EBD871D7FF38200E6A9CA /* OAuth2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EBD7E1D7FF38200E6A9CA /* OAuth2Tests.swift */; }; + EE4EBD881D7FF38200E6A9CA /* OAuth2AuthRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EBD7F1D7FF38200E6A9CA /* OAuth2AuthRequestTests.swift */; }; + EE4EBD891D7FF38200E6A9CA /* OAuth2ClientCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EBD811D7FF38200E6A9CA /* OAuth2ClientCredentialsTests.swift */; }; + EE4EBD8A1D7FF38200E6A9CA /* OAuth2CodeGrantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EBD821D7FF38200E6A9CA /* OAuth2CodeGrantTests.swift */; }; + EE4EBD8B1D7FF38200E6A9CA /* OAuth2DynRegTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EBD831D7FF38200E6A9CA /* OAuth2DynRegTests.swift */; }; + EE4EBD8C1D7FF38200E6A9CA /* OAuth2ImplicitGrantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EBD841D7FF38200E6A9CA /* OAuth2ImplicitGrantTests.swift */; }; + EE4EBD8D1D7FF38200E6A9CA /* OAuth2PasswordGrantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EBD851D7FF38200E6A9CA /* OAuth2PasswordGrantTests.swift */; }; + EE4EBD8E1D7FF38200E6A9CA /* OAuth2RefreshTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EBD861D7FF38200E6A9CA /* OAuth2RefreshTokenTests.swift */; }; EE507A381B1E15E000AE02E9 /* OAuth2DynReg.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE507A371B1E15E000AE02E9 /* OAuth2DynReg.swift */; }; EE507A391B1E15E000AE02E9 /* OAuth2DynReg.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE507A371B1E15E000AE02E9 /* OAuth2DynReg.swift */; }; EE5F4F861B19114D00DB38B3 /* OAuth2ClientCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5F4F851B19114D00DB38B3 /* OAuth2ClientCredentials.swift */; }; EE5F4F871B19114D00DB38B3 /* OAuth2ClientCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5F4F851B19114D00DB38B3 /* OAuth2ClientCredentials.swift */; }; - EE5F4F891B1913D400DB38B3 /* OAuth2ClientCredentials_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5F4F881B1913D400DB38B3 /* OAuth2ClientCredentials_tests.swift */; }; EE79F6541BFA93D900746243 /* OAuth2AuthConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE79F6531BFA93D900746243 /* OAuth2AuthConfig.swift */; }; EE79F6551BFA93D900746243 /* OAuth2AuthConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE79F6531BFA93D900746243 /* OAuth2AuthConfig.swift */; }; EE79F6571BFA945C00746243 /* OAuth2ClientConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE79F6561BFA945C00746243 /* OAuth2ClientConfig.swift */; }; @@ -50,15 +60,23 @@ EE79F65B1BFAA36900746243 /* OAuth2Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE79F6591BFAA36900746243 /* OAuth2Error.swift */; }; EE86C4651C48F6AC00B7D486 /* OAuth2CodeGrantNoTokenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE86C4641C48F6AC00B7D486 /* OAuth2CodeGrantNoTokenType.swift */; }; EE86C4661C48F6AC00B7D486 /* OAuth2CodeGrantNoTokenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE86C4641C48F6AC00B7D486 /* OAuth2CodeGrantNoTokenType.swift */; }; - EE92BE591C2700B600C8720B /* OAuth2RefreshToken_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92BE581C2700B600C8720B /* OAuth2RefreshToken_tests.swift */; }; - EEACE1D41A7E8DE8009BF3A7 /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB8640193FAB9200C4EEA1 /* OAuth2.swift */; }; - EEACE1D51A7E8DE8009BF3A7 /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB8640193FAB9200C4EEA1 /* OAuth2.swift */; }; + EE9EBF131D775A21003263FC /* OAuth2DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF121D775A21003263FC /* OAuth2DataLoader.swift */; }; + EE9EBF141D775A21003263FC /* OAuth2DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF121D775A21003263FC /* OAuth2DataLoader.swift */; }; + EE9EBF151D775A21003263FC /* OAuth2DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF121D775A21003263FC /* OAuth2DataLoader.swift */; }; + EE9EBF171D775C59003263FC /* OAuth2DataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF161D775C59003263FC /* OAuth2DataRequest.swift */; }; + EE9EBF181D775C59003263FC /* OAuth2DataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF161D775C59003263FC /* OAuth2DataRequest.swift */; }; + EE9EBF191D775C59003263FC /* OAuth2DataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF161D775C59003263FC /* OAuth2DataRequest.swift */; }; + EE9EBF1B1D775F74003263FC /* OAuth2Securable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF1A1D775F74003263FC /* OAuth2Securable.swift */; }; + EE9EBF1C1D775F74003263FC /* OAuth2Securable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF1A1D775F74003263FC /* OAuth2Securable.swift */; }; + EE9EBF1D1D775F74003263FC /* OAuth2Securable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9EBF1A1D775F74003263FC /* OAuth2Securable.swift */; }; + EEACE1D41A7E8DE8009BF3A7 /* OAuth2Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB8640193FAB9200C4EEA1 /* OAuth2Base.swift */; }; + EEACE1D51A7E8DE8009BF3A7 /* OAuth2Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB8640193FAB9200C4EEA1 /* OAuth2Base.swift */; }; EEACE1D61A7E8DEB009BF3A7 /* OAuth2ImplicitGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3174EB1945E83100210E62 /* OAuth2ImplicitGrant.swift */; }; EEACE1D71A7E8DEB009BF3A7 /* OAuth2ImplicitGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3174EB1945E83100210E62 /* OAuth2ImplicitGrant.swift */; }; EEACE1D81A7E8DEE009BF3A7 /* OAuth2CodeGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE44F691194F2C7D0094AB8B /* OAuth2CodeGrant.swift */; }; EEACE1D91A7E8DEE009BF3A7 /* OAuth2CodeGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE44F691194F2C7D0094AB8B /* OAuth2CodeGrant.swift */; }; - EEACE1DA1A7E8DF1009BF3A7 /* OAuth2Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29EABB195A0DB2008882C8 /* OAuth2Request.swift */; }; - EEACE1DB1A7E8DF1009BF3A7 /* OAuth2Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29EABB195A0DB2008882C8 /* OAuth2Request.swift */; }; + EEACE1DA1A7E8DF1009BF3A7 /* OAuth2DebugURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29EABB195A0DB2008882C8 /* OAuth2DebugURLSessionDelegate.swift */; }; + EEACE1DB1A7E8DF1009BF3A7 /* OAuth2DebugURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE29EABB195A0DB2008882C8 /* OAuth2DebugURLSessionDelegate.swift */; }; EEACE1DC1A7E8DF6009BF3A7 /* extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB864B19421DAD00C4EEA1 /* extensions.swift */; }; EEACE1DD1A7E8DF7009BF3A7 /* extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB864B19421DAD00C4EEA1 /* extensions.swift */; }; EEACE1DF1A7E8FC1009BF3A7 /* OAuth2CodeGrantFacebook.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEACE1DE1A7E8FC1009BF3A7 /* OAuth2CodeGrantFacebook.swift */; }; @@ -66,21 +84,25 @@ EEAEF10B1CDBCF28001A1C6F /* OAuth2Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAEF10A1CDBCF28001A1C6F /* OAuth2Logger.swift */; }; EEAEF10C1CDBCF28001A1C6F /* OAuth2Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAEF10A1CDBCF28001A1C6F /* OAuth2Logger.swift */; }; EEAEF10D1CDBCF28001A1C6F /* OAuth2Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAEF10A1CDBCF28001A1C6F /* OAuth2Logger.swift */; }; - EEBA36AC1A8D030F00CF4230 /* OAuth2ImplicitGrant_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBA36AB1A8D030F00CF4230 /* OAuth2ImplicitGrant_tests.swift */; }; + EEB9A97C1D86C34E0022EF66 /* OAuth2Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9A97B1D86C34E0022EF66 /* OAuth2Response.swift */; }; + EEB9A97D1D86C34E0022EF66 /* OAuth2Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9A97B1D86C34E0022EF66 /* OAuth2Response.swift */; }; + EEB9A97E1D86C34E0022EF66 /* OAuth2Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9A97B1D86C34E0022EF66 /* OAuth2Response.swift */; }; + EEB9A9801D86CD4A0022EF66 /* OAuth2DataLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9A97F1D86CD4A0022EF66 /* OAuth2DataLoaderTests.swift */; }; + EEB9A9831D86D36A0022EF66 /* OAuth2RequestPerformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9A9821D86D36A0022EF66 /* OAuth2RequestPerformer.swift */; }; + EEB9A9841D86D36A0022EF66 /* OAuth2RequestPerformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9A9821D86D36A0022EF66 /* OAuth2RequestPerformer.swift */; }; + EEB9A9851D86D36A0022EF66 /* OAuth2RequestPerformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9A9821D86D36A0022EF66 /* OAuth2RequestPerformer.swift */; }; EEC49F311C9BF22400989A18 /* OAuth2AuthRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC49F301C9BF22400989A18 /* OAuth2AuthRequest.swift */; }; EEC49F321C9BF22400989A18 /* OAuth2AuthRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC49F301C9BF22400989A18 /* OAuth2AuthRequest.swift */; }; EEC49F331C9BF22400989A18 /* OAuth2AuthRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC49F301C9BF22400989A18 /* OAuth2AuthRequest.swift */; }; - EEC49F351C9C264000989A18 /* OAuth2AuthRequest_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC49F341C9C264000989A18 /* OAuth2AuthRequest_tests.swift */; }; EEC6D57C1C2837EA00FA9B1C /* OAuth2CodeGrantLinkedIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC6D57B1C2837EA00FA9B1C /* OAuth2CodeGrantLinkedIn.swift */; }; EEC6D57D1C2837EA00FA9B1C /* OAuth2CodeGrantLinkedIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC6D57B1C2837EA00FA9B1C /* OAuth2CodeGrantLinkedIn.swift */; }; - EEC7A8C71AE46C33008C30E7 /* OAuth2+OSX.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7A8C61AE46C33008C30E7 /* OAuth2+OSX.swift */; }; - EEC7A8C91AE47111008C30E7 /* OAuth2+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7A8C81AE47111008C30E7 /* OAuth2+iOS.swift */; }; + EEC7A8C71AE46C33008C30E7 /* OAuth2Authorizer+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7A8C61AE46C33008C30E7 /* OAuth2Authorizer+macOS.swift */; }; + EEC7A8C91AE47111008C30E7 /* OAuth2Authorizer+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7A8C81AE47111008C30E7 /* OAuth2Authorizer+iOS.swift */; }; EEC7A8D81AE4851E008C30E7 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7A8D01AE4851E008C30E7 /* Keychain.swift */; }; EEC7A8D91AE4851E008C30E7 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7A8D01AE4851E008C30E7 /* Keychain.swift */; }; - EEE209A719427DFE00736F1A /* OAuth2_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE209A619427DFE00736F1A /* OAuth2_tests.swift */; }; EEE209A819427DFE00736F1A /* OAuth2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEE209471942772800736F1A /* OAuth2.framework */; }; - EEF47D2B1B1E3FDD0057D838 /* OAuth2Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF47D2A1B1E3FDD0057D838 /* OAuth2Base.swift */; }; - EEF47D2C1B1E3FDD0057D838 /* OAuth2Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF47D2A1B1E3FDD0057D838 /* OAuth2Base.swift */; }; + EEF47D2B1B1E3FDD0057D838 /* OAuth2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF47D2A1B1E3FDD0057D838 /* OAuth2Requestable.swift */; }; + EEF47D2C1B1E3FDD0057D838 /* OAuth2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF47D2A1B1E3FDD0057D838 /* OAuth2Requestable.swift */; }; EEFD23511C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFD23501C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift */; }; EEFD23521C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFD23501C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift */; }; EEFD23531C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFD23501C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift */; }; @@ -132,52 +154,61 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 6598543F1C5B3B4000237D39 /* OAuth2+tvOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "OAuth2+tvOS.swift"; path = "Sources/tvOS/OAuth2+tvOS.swift"; sourceTree = SOURCE_ROOT; }; + 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 = ""; }; DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2WebViewController.swift; sourceTree = ""; }; EA9758171B222CEA007744B1 /* OAuth2PasswordGrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2PasswordGrant.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - EA97581B1B223932007744B1 /* OAuth2PasswordGrant_tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2PasswordGrant_tests.swift; sourceTree = ""; }; EE01F96E1C58D5D6003AEA7E /* generate-docs.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "generate-docs.sh"; sourceTree = ""; }; EE1391D91AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2CodeGrantBasicAuth.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE2486291AC85DD4002B31AF /* OAuth2WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2WebViewController.swift; sourceTree = ""; }; - EE29EABB195A0DB2008882C8 /* OAuth2Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Request.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 = ""; }; + EE29EABB195A0DB2008882C8 /* OAuth2DebugURLSessionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2DebugURLSessionDelegate.swift; sourceTree = ""; }; EE29EABE195B0813008882C8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; EE3174EB1945E83100210E62 /* OAuth2ImplicitGrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2ImplicitGrant.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - EE414B6C1C0F6BDE00DE6A8F /* OAuth2DynReg_tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2DynReg_tests.swift; sourceTree = ""; }; - EE43B1591951FAC70017679A /* OAuth2CodeGrant_tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CodeGrant_tests.swift; sourceTree = ""; }; EE44F691194F2C7D0094AB8B /* OAuth2CodeGrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2CodeGrant.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE450CD31AD57578008AB6FC /* p2.OAuth2.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = p2.OAuth2.podspec; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + EE4EBD7E1D7FF38200E6A9CA /* OAuth2Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Tests.swift; sourceTree = ""; }; + EE4EBD7F1D7FF38200E6A9CA /* OAuth2AuthRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2AuthRequestTests.swift; sourceTree = ""; }; + EE4EBD811D7FF38200E6A9CA /* OAuth2ClientCredentialsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2ClientCredentialsTests.swift; sourceTree = ""; }; + EE4EBD821D7FF38200E6A9CA /* OAuth2CodeGrantTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CodeGrantTests.swift; sourceTree = ""; }; + EE4EBD831D7FF38200E6A9CA /* OAuth2DynRegTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2DynRegTests.swift; sourceTree = ""; }; + EE4EBD841D7FF38200E6A9CA /* OAuth2ImplicitGrantTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2ImplicitGrantTests.swift; sourceTree = ""; }; + EE4EBD851D7FF38200E6A9CA /* OAuth2PasswordGrantTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2PasswordGrantTests.swift; sourceTree = ""; }; + EE4EBD861D7FF38200E6A9CA /* OAuth2RefreshTokenTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2RefreshTokenTests.swift; sourceTree = ""; }; EE507A371B1E15E000AE02E9 /* OAuth2DynReg.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2DynReg.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE5F4F851B19114D00DB38B3 /* OAuth2ClientCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2ClientCredentials.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - EE5F4F881B1913D400DB38B3 /* OAuth2ClientCredentials_tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2ClientCredentials_tests.swift; sourceTree = ""; }; EE79F6531BFA93D900746243 /* OAuth2AuthConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2AuthConfig.swift; sourceTree = ""; }; EE79F6561BFA945C00746243 /* OAuth2ClientConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2ClientConfig.swift; sourceTree = ""; }; EE79F6591BFAA36900746243 /* OAuth2Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Error.swift; sourceTree = ""; }; EE86C4641C48F6AC00B7D486 /* OAuth2CodeGrantNoTokenType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CodeGrantNoTokenType.swift; sourceTree = ""; }; EE8EE6DA1B12AB35005B90C5 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; - EE92BE581C2700B600C8720B /* OAuth2RefreshToken_tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2RefreshToken_tests.swift; sourceTree = ""; }; + EE9EBF121D775A21003263FC /* OAuth2DataLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2DataLoader.swift; sourceTree = ""; }; + EE9EBF161D775C59003263FC /* OAuth2DataRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2DataRequest.swift; sourceTree = ""; }; + EE9EBF1A1D775F74003263FC /* OAuth2Securable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Securable.swift; sourceTree = ""; }; EEAC179F1CAA69110025F84B /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; EEAC17A01CAA69110025F84B /* CONTRIBUTORS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTORS.md; sourceTree = ""; }; EEACE1DE1A7E8FC1009BF3A7 /* OAuth2CodeGrantFacebook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CodeGrantFacebook.swift; sourceTree = ""; }; EEAEF10A1CDBCF28001A1C6F /* OAuth2Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Logger.swift; sourceTree = ""; }; - EEBA36AB1A8D030F00CF4230 /* OAuth2ImplicitGrant_tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2ImplicitGrant_tests.swift; sourceTree = ""; }; + EEB9A97B1D86C34E0022EF66 /* OAuth2Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Response.swift; sourceTree = ""; }; + EEB9A97F1D86CD4A0022EF66 /* OAuth2DataLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2DataLoaderTests.swift; sourceTree = ""; }; + EEB9A9821D86D36A0022EF66 /* OAuth2RequestPerformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2RequestPerformer.swift; sourceTree = ""; }; EEC49F301C9BF22400989A18 /* OAuth2AuthRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2AuthRequest.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - EEC49F341C9C264000989A18 /* OAuth2AuthRequest_tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2AuthRequest_tests.swift; sourceTree = ""; }; EEC6D57B1C2837EA00FA9B1C /* OAuth2CodeGrantLinkedIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CodeGrantLinkedIn.swift; sourceTree = ""; }; - EEC7A8C61AE46C33008C30E7 /* OAuth2+OSX.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "OAuth2+OSX.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - EEC7A8C81AE47111008C30E7 /* OAuth2+iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "OAuth2+iOS.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - EEC7A8D01AE4851E008C30E7 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; + EEC7A8C61AE46C33008C30E7 /* OAuth2Authorizer+macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "OAuth2Authorizer+macOS.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + EEC7A8C81AE47111008C30E7 /* OAuth2Authorizer+iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "OAuth2Authorizer+iOS.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + EEC7A8D01AE4851E008C30E7 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Keychain.swift; path = ../../SwiftKeychain/Keychain/Keychain.swift; sourceTree = ""; }; EEC7A8E01AE48533008C30E7 /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; EEDB8624193FAAE500C4EEA1 /* OAuth2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OAuth2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - EEDB8640193FAB9200C4EEA1 /* OAuth2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + EEDB8640193FAB9200C4EEA1 /* OAuth2Base.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2Base.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EEDB864B19421DAD00C4EEA1 /* extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = extensions.swift; sourceTree = ""; }; EEE209471942772800736F1A /* OAuth2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OAuth2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EEE2098B1942778300736F1A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EEE209A219427DFE00736F1A /* OAuth2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OAuth2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EEE209A519427DFE00736F1A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - EEE209A619427DFE00736F1A /* OAuth2_tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2_tests.swift; sourceTree = ""; }; - EEF47D2A1B1E3FDD0057D838 /* OAuth2Base.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2Base.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + EEF47D2A1B1E3FDD0057D838 /* OAuth2Requestable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OAuth2Requestable.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EEFD23501C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2ClientCredentialsReddit.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -217,24 +248,69 @@ 65D294B91C57CB47009DA970 /* tvOS */ = { isa = PBXGroup; children = ( - 6598543F1C5B3B4000237D39 /* OAuth2+tvOS.swift */, + 6598543F1C5B3B4000237D39 /* OAuth2Authorizer+tvOS.swift */, ); - name = tvOS; + path = tvOS; sourceTree = ""; }; EE2486281AC85DD4002B31AF /* iOS */ = { isa = PBXGroup; children = ( - EEC7A8C81AE47111008C30E7 /* OAuth2+iOS.swift */, + EEC7A8C81AE47111008C30E7 /* OAuth2Authorizer+iOS.swift */, EE2486291AC85DD4002B31AF /* OAuth2WebViewController.swift */, ); - name = iOS; - path = ../iOS; + path = iOS; + sourceTree = ""; + }; + EE2983731D40BC8900933CDD /* Base */ = { + isa = PBXGroup; + children = ( + EEDB8640193FAB9200C4EEA1 /* OAuth2Base.swift */, + EE9EBF1A1D775F74003263FC /* OAuth2Securable.swift */, + EEF47D2A1B1E3FDD0057D838 /* OAuth2Requestable.swift */, + EE79F6561BFA945C00746243 /* OAuth2ClientConfig.swift */, + EE79F6531BFA93D900746243 /* OAuth2AuthConfig.swift */, + EEC49F301C9BF22400989A18 /* OAuth2AuthRequest.swift */, + EEB9A9821D86D36A0022EF66 /* OAuth2RequestPerformer.swift */, + EEB9A97B1D86C34E0022EF66 /* OAuth2Response.swift */, + EE79F6591BFAA36900746243 /* OAuth2Error.swift */, + EEAEF10A1CDBCF28001A1C6F /* OAuth2Logger.swift */, + 65EC05DF1C9050CB00DE9186 /* OAuth2KeychainAccount.swift */, + EE29EABB195A0DB2008882C8 /* OAuth2DebugURLSessionDelegate.swift */, + EE2983741D40BE7600933CDD /* OAuth2AuthorizerUI.swift */, + EEDB864B19421DAD00C4EEA1 /* extensions.swift */, + ); + path = Base; + sourceTree = ""; + }; + EE4EBD7D1D7FF38200E6A9CA /* Base */ = { + isa = PBXGroup; + children = ( + EE4EBD7E1D7FF38200E6A9CA /* OAuth2Tests.swift */, + EE4EBD7F1D7FF38200E6A9CA /* OAuth2AuthRequestTests.swift */, + ); + name = Base; + path = BaseTests; + sourceTree = ""; + }; + EE4EBD801D7FF38200E6A9CA /* Flows */ = { + isa = PBXGroup; + children = ( + EE4EBD811D7FF38200E6A9CA /* OAuth2ClientCredentialsTests.swift */, + EE4EBD821D7FF38200E6A9CA /* OAuth2CodeGrantTests.swift */, + EE4EBD831D7FF38200E6A9CA /* OAuth2DynRegTests.swift */, + EE4EBD841D7FF38200E6A9CA /* OAuth2ImplicitGrantTests.swift */, + EE4EBD851D7FF38200E6A9CA /* OAuth2PasswordGrantTests.swift */, + EE4EBD861D7FF38200E6A9CA /* OAuth2RefreshTokenTests.swift */, + ); + name = Flows; + path = FlowTests; sourceTree = ""; }; EE79F65C1BFBDFFF00746243 /* Flows */ = { isa = PBXGroup; children = ( + EE29836F1D40B83600933CDD /* OAuth2.swift */, EE3174EB1945E83100210E62 /* OAuth2ImplicitGrant.swift */, EE44F691194F2C7D0094AB8B /* OAuth2CodeGrant.swift */, EEACE1DE1A7E8FC1009BF3A7 /* OAuth2CodeGrantFacebook.swift */, @@ -244,18 +320,36 @@ EE5F4F851B19114D00DB38B3 /* OAuth2ClientCredentials.swift */, EEFD23501C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift */, EA9758171B222CEA007744B1 /* OAuth2PasswordGrant.swift */, + EE507A371B1E15E000AE02E9 /* OAuth2DynReg.swift */, ); - name = Flows; + path = Flows; + sourceTree = ""; + }; + EE9EBF111D775A21003263FC /* DataLoader */ = { + isa = PBXGroup; + children = ( + EE9EBF121D775A21003263FC /* OAuth2DataLoader.swift */, + EE9EBF161D775C59003263FC /* OAuth2DataRequest.swift */, + ); + path = DataLoader; sourceTree = ""; }; - EEC7A8C51AE46C33008C30E7 /* OSX */ = { + EEB9A9811D86CD540022EF66 /* DataLoader */ = { isa = PBXGroup; children = ( - EEC7A8C61AE46C33008C30E7 /* OAuth2+OSX.swift */, + EEB9A97F1D86CD4A0022EF66 /* OAuth2DataLoaderTests.swift */, + ); + name = DataLoader; + path = DataLoaderTests; + sourceTree = ""; + }; + EEC7A8C51AE46C33008C30E7 /* macOS */ = { + isa = PBXGroup; + children = ( + EEC7A8C61AE46C33008C30E7 /* OAuth2Authorizer+macOS.swift */, DD0CCBAC1C4DC83A0044C4E3 /* OAuth2WebViewController.swift */, ); - name = OSX; - path = ../OSX; + path = macOS; sourceTree = ""; }; EEC7A8CD1AE4851E008C30E7 /* SwiftKeychain */ = { @@ -263,8 +357,7 @@ children = ( EEC7A8D01AE4851E008C30E7 /* Keychain.swift */, ); - name = SwiftKeychain; - path = ../../SwiftKeychain/Keychain; + path = SwiftKeychain; sourceTree = ""; }; EEDB861A193FAAE500C4EEA1 = { @@ -274,6 +367,7 @@ EE8EE6DA1B12AB35005B90C5 /* CHANGELOG.md */, EEAC179F1CAA69110025F84B /* CONTRIBUTING.md */, EEAC17A01CAA69110025F84B /* CONTRIBUTORS.md */, + EE29836A1D3FC28000933CDD /* Package.swift */, EE450CD31AD57578008AB6FC /* p2.OAuth2.podspec */, EE01F96E1C58D5D6003AEA7E /* generate-docs.sh */, EEDB8626193FAAE500C4EEA1 /* OAuth2 */, @@ -297,26 +391,17 @@ EEDB8626193FAAE500C4EEA1 /* OAuth2 */ = { isa = PBXGroup; children = ( - EEDB8640193FAB9200C4EEA1 /* OAuth2.swift */, - EEF47D2A1B1E3FDD0057D838 /* OAuth2Base.swift */, - EE79F6561BFA945C00746243 /* OAuth2ClientConfig.swift */, - EE79F6531BFA93D900746243 /* OAuth2AuthConfig.swift */, - EEC49F301C9BF22400989A18 /* OAuth2AuthRequest.swift */, + EE2983731D40BC8900933CDD /* Base */, EE79F65C1BFBDFFF00746243 /* Flows */, - EE507A371B1E15E000AE02E9 /* OAuth2DynReg.swift */, - EE29EABB195A0DB2008882C8 /* OAuth2Request.swift */, - EE79F6591BFAA36900746243 /* OAuth2Error.swift */, - EEAEF10A1CDBCF28001A1C6F /* OAuth2Logger.swift */, - 65EC05DF1C9050CB00DE9186 /* OAuth2KeychainAccount.swift */, - EEDB864B19421DAD00C4EEA1 /* extensions.swift */, + EE9EBF111D775A21003263FC /* DataLoader */, EE2486281AC85DD4002B31AF /* iOS */, - EEC7A8C51AE46C33008C30E7 /* OSX */, + EEC7A8C51AE46C33008C30E7 /* macOS */, 65D294B91C57CB47009DA970 /* tvOS */, EEC7A8CD1AE4851E008C30E7 /* SwiftKeychain */, EEDB8627193FAAE500C4EEA1 /* Supporting Files */, ); name = OAuth2; - path = Sources/Base; + path = Sources; sourceTree = ""; }; EEDB8627193FAAE500C4EEA1 /* Supporting Files */ = { @@ -332,14 +417,9 @@ EEE209A319427DFE00736F1A /* Tests */ = { isa = PBXGroup; children = ( - EEE209A619427DFE00736F1A /* OAuth2_tests.swift */, - EE92BE581C2700B600C8720B /* OAuth2RefreshToken_tests.swift */, - EEBA36AB1A8D030F00CF4230 /* OAuth2ImplicitGrant_tests.swift */, - EE43B1591951FAC70017679A /* OAuth2CodeGrant_tests.swift */, - EE5F4F881B1913D400DB38B3 /* OAuth2ClientCredentials_tests.swift */, - EA97581B1B223932007744B1 /* OAuth2PasswordGrant_tests.swift */, - EE414B6C1C0F6BDE00DE6A8F /* OAuth2DynReg_tests.swift */, - EEC49F341C9C264000989A18 /* OAuth2AuthRequest_tests.swift */, + EE4EBD7D1D7FF38200E6A9CA /* Base */, + EE4EBD801D7FF38200E6A9CA /* Flows */, + EEB9A9811D86CD540022EF66 /* DataLoader */, EEE209A419427DFE00736F1A /* Supporting Files */, ); path = Tests; @@ -416,9 +496,9 @@ productReference = EEDB8624193FAAE500C4EEA1 /* OAuth2.framework */; productType = "com.apple.product-type.framework"; }; - EEE209461942772800736F1A /* OAuth2OSX */ = { + EEE209461942772800736F1A /* OAuth2macOS */ = { isa = PBXNativeTarget; - buildConfigurationList = EEE2095A1942772800736F1A /* Build configuration list for PBXNativeTarget "OAuth2OSX" */; + buildConfigurationList = EEE2095A1942772800736F1A /* Build configuration list for PBXNativeTarget "OAuth2macOS" */; buildPhases = ( EEE209421942772800736F1A /* Sources */, EEE209431942772800736F1A /* Frameworks */, @@ -429,7 +509,7 @@ ); dependencies = ( ); - name = OAuth2OSX; + name = OAuth2macOS; productName = OAuth2; productReference = EEE209471942772800736F1A /* OAuth2.framework */; productType = "com.apple.product-type.framework"; @@ -473,7 +553,6 @@ }; EEDB8623193FAAE500C4EEA1 = { CreatedOnToolsVersion = 6.0; - LastSwiftMigration = 0800; }; EEE209461942772800736F1A = { CreatedOnToolsVersion = 6.0; @@ -498,7 +577,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - EEE209461942772800736F1A /* OAuth2OSX */, + EEE209461942772800736F1A /* OAuth2macOS */, EEDB8623193FAAE500C4EEA1 /* OAuth2iOS */, 659854451C5B3BEA00237D39 /* OAuth2tvOS */, EEE209A119427DFE00736F1A /* OAuth2Tests */, @@ -542,23 +621,30 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + EE9EBF191D775C59003263FC /* OAuth2DataRequest.swift in Sources */, 659854591C5B3CA700237D39 /* OAuth2ClientCredentials.swift in Sources */, + EE2983771D40BE7600933CDD /* OAuth2AuthorizerUI.swift in Sources */, 6598545E1C5B3CAB00237D39 /* extensions.swift in Sources */, 659854541C5B3CA700237D39 /* OAuth2CodeGrant.swift in Sources */, 659854571C5B3CA700237D39 /* OAuth2CodeGrantBasicAuth.swift in Sources */, 659854531C5B3CA700237D39 /* OAuth2ImplicitGrant.swift in Sources */, - 659854501C5B3C9C00237D39 /* OAuth2Base.swift in Sources */, - 6598544F1C5B3C9C00237D39 /* OAuth2.swift in Sources */, + 659854501C5B3C9C00237D39 /* OAuth2Requestable.swift in Sources */, + 6598544F1C5B3C9C00237D39 /* OAuth2Base.swift in Sources */, + EEB9A97E1D86C34E0022EF66 /* OAuth2Response.swift in Sources */, EEFD23531C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift in Sources */, 6598545A1C5B3CA700237D39 /* OAuth2PasswordGrant.swift in Sources */, 659854551C5B3CA700237D39 /* OAuth2CodeGrantFacebook.swift in Sources */, 659854611C5B3CB900237D39 /* Keychain.swift in Sources */, + EEB9A9851D86D36A0022EF66 /* OAuth2RequestPerformer.swift in Sources */, 659854561C5B3CA700237D39 /* OAuth2CodeGrantLinkedIn.swift in Sources */, 6598545D1C5B3CAB00237D39 /* OAuth2Error.swift in Sources */, + EE2983721D40B83600933CDD /* OAuth2.swift in Sources */, 65EC05E21C9050CB00DE9186 /* OAuth2KeychainAccount.swift in Sources */, - 6598545C1C5B3CAB00237D39 /* OAuth2Request.swift in Sources */, - 6598544E1C5B3C9500237D39 /* OAuth2+tvOS.swift in Sources */, + 6598545C1C5B3CAB00237D39 /* OAuth2DebugURLSessionDelegate.swift in Sources */, + 6598544E1C5B3C9500237D39 /* OAuth2Authorizer+tvOS.swift in Sources */, + EE9EBF151D775A21003263FC /* OAuth2DataLoader.swift in Sources */, EEAEF10D1CDBCF28001A1C6F /* OAuth2Logger.swift in Sources */, + EE9EBF1D1D775F74003263FC /* OAuth2Securable.swift in Sources */, 659854521C5B3C9C00237D39 /* OAuth2AuthConfig.swift in Sources */, EEC49F331C9BF22400989A18 /* OAuth2AuthRequest.swift in Sources */, 659854511C5B3C9C00237D39 /* OAuth2ClientConfig.swift in Sources */, @@ -576,18 +662,25 @@ EEAEF10C1CDBCF28001A1C6F /* OAuth2Logger.swift in Sources */, 65EC05E11C9050CB00DE9186 /* OAuth2KeychainAccount.swift in Sources */, EE86C4661C48F6AC00B7D486 /* OAuth2CodeGrantNoTokenType.swift in Sources */, - EEF47D2C1B1E3FDD0057D838 /* OAuth2Base.swift in Sources */, + EE9EBF1C1D775F74003263FC /* OAuth2Securable.swift in Sources */, + EEF47D2C1B1E3FDD0057D838 /* OAuth2Requestable.swift in Sources */, EEC49F321C9BF22400989A18 /* OAuth2AuthRequest.swift in Sources */, EEC6D57D1C2837EA00FA9B1C /* OAuth2CodeGrantLinkedIn.swift in Sources */, EE79F6551BFA93D900746243 /* OAuth2AuthConfig.swift in Sources */, - EEACE1D51A7E8DE8009BF3A7 /* OAuth2.swift in Sources */, - EEC7A8C91AE47111008C30E7 /* OAuth2+iOS.swift in Sources */, - EEACE1DB1A7E8DF1009BF3A7 /* OAuth2Request.swift in Sources */, + EEACE1D51A7E8DE8009BF3A7 /* OAuth2Base.swift in Sources */, + EEC7A8C91AE47111008C30E7 /* OAuth2Authorizer+iOS.swift in Sources */, + EE2983761D40BE7600933CDD /* OAuth2AuthorizerUI.swift in Sources */, + EE9EBF181D775C59003263FC /* OAuth2DataRequest.swift in Sources */, + EEACE1DB1A7E8DF1009BF3A7 /* OAuth2DebugURLSessionDelegate.swift in Sources */, + EEB9A9841D86D36A0022EF66 /* OAuth2RequestPerformer.swift in Sources */, EA97581E1B2242F9007744B1 /* OAuth2PasswordGrant.swift in Sources */, EEACE1D71A7E8DEB009BF3A7 /* OAuth2ImplicitGrant.swift in Sources */, EE5F4F871B19114D00DB38B3 /* OAuth2ClientCredentials.swift in Sources */, EEFD23521C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift in Sources */, + EE2983711D40B83600933CDD /* OAuth2.swift in Sources */, + EEB9A97D1D86C34E0022EF66 /* OAuth2Response.swift in Sources */, EEACE1D91A7E8DEE009BF3A7 /* OAuth2CodeGrant.swift in Sources */, + EE9EBF141D775A21003263FC /* OAuth2DataLoader.swift in Sources */, EEACE1E01A7E8FC5009BF3A7 /* OAuth2CodeGrantFacebook.swift in Sources */, EE1391DB1AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift in Sources */, EE507A391B1E15E000AE02E9 /* OAuth2DynReg.swift in Sources */, @@ -606,18 +699,25 @@ EEAEF10B1CDBCF28001A1C6F /* OAuth2Logger.swift in Sources */, 65EC05E01C9050CB00DE9186 /* OAuth2KeychainAccount.swift in Sources */, DD0CCBAD1C4DC83A0044C4E3 /* OAuth2WebViewController.swift in Sources */, + EE9EBF1B1D775F74003263FC /* OAuth2Securable.swift in Sources */, EE79F65A1BFAA36900746243 /* OAuth2Error.swift in Sources */, EEC49F311C9BF22400989A18 /* OAuth2AuthRequest.swift in Sources */, EE79F6571BFA945C00746243 /* OAuth2ClientConfig.swift in Sources */, EE79F6541BFA93D900746243 /* OAuth2AuthConfig.swift in Sources */, - EEF47D2B1B1E3FDD0057D838 /* OAuth2Base.swift in Sources */, - EEACE1D41A7E8DE8009BF3A7 /* OAuth2.swift in Sources */, - EEC7A8C71AE46C33008C30E7 /* OAuth2+OSX.swift in Sources */, - EEACE1DA1A7E8DF1009BF3A7 /* OAuth2Request.swift in Sources */, + EEF47D2B1B1E3FDD0057D838 /* OAuth2Requestable.swift in Sources */, + EEACE1D41A7E8DE8009BF3A7 /* OAuth2Base.swift in Sources */, + EE2983751D40BE7600933CDD /* OAuth2AuthorizerUI.swift in Sources */, + EE9EBF171D775C59003263FC /* OAuth2DataRequest.swift in Sources */, + EEC7A8C71AE46C33008C30E7 /* OAuth2Authorizer+macOS.swift in Sources */, + EEB9A9831D86D36A0022EF66 /* OAuth2RequestPerformer.swift in Sources */, + EEACE1DA1A7E8DF1009BF3A7 /* OAuth2DebugURLSessionDelegate.swift in Sources */, EE5F4F861B19114D00DB38B3 /* OAuth2ClientCredentials.swift in Sources */, EEACE1D61A7E8DEB009BF3A7 /* OAuth2ImplicitGrant.swift in Sources */, EEFD23511C9ED9E400727DCF /* OAuth2ClientCredentialsReddit.swift in Sources */, + EE2983701D40B83600933CDD /* OAuth2.swift in Sources */, + EEB9A97C1D86C34E0022EF66 /* OAuth2Response.swift in Sources */, EEACE1D81A7E8DEE009BF3A7 /* OAuth2CodeGrant.swift in Sources */, + EE9EBF131D775A21003263FC /* OAuth2DataLoader.swift in Sources */, EE86C4651C48F6AC00B7D486 /* OAuth2CodeGrantNoTokenType.swift in Sources */, EEACE1DF1A7E8FC1009BF3A7 /* OAuth2CodeGrantFacebook.swift in Sources */, EEC6D57C1C2837EA00FA9B1C /* OAuth2CodeGrantLinkedIn.swift in Sources */, @@ -631,14 +731,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EEC49F351C9C264000989A18 /* OAuth2AuthRequest_tests.swift in Sources */, - EE5F4F891B1913D400DB38B3 /* OAuth2ClientCredentials_tests.swift in Sources */, - EE43B15F1952E4700017679A /* OAuth2CodeGrant_tests.swift in Sources */, - EEBA36AC1A8D030F00CF4230 /* OAuth2ImplicitGrant_tests.swift in Sources */, - EE414B6D1C0F6BDE00DE6A8F /* OAuth2DynReg_tests.swift in Sources */, - EA97581D1B223A5D007744B1 /* OAuth2PasswordGrant_tests.swift in Sources */, - EE92BE591C2700B600C8720B /* OAuth2RefreshToken_tests.swift in Sources */, - EEE209A719427DFE00736F1A /* OAuth2_tests.swift in Sources */, + EE4EBD8D1D7FF38200E6A9CA /* OAuth2PasswordGrantTests.swift in Sources */, + EE4EBD881D7FF38200E6A9CA /* OAuth2AuthRequestTests.swift in Sources */, + EE4EBD891D7FF38200E6A9CA /* OAuth2ClientCredentialsTests.swift in Sources */, + EE4EBD8B1D7FF38200E6A9CA /* OAuth2DynRegTests.swift in Sources */, + EE4EBD8A1D7FF38200E6A9CA /* OAuth2CodeGrantTests.swift in Sources */, + EEB9A9801D86CD4A0022EF66 /* OAuth2DataLoaderTests.swift in Sources */, + EE4EBD8C1D7FF38200E6A9CA /* OAuth2ImplicitGrantTests.swift in Sources */, + EE4EBD8E1D7FF38200E6A9CA /* OAuth2RefreshTokenTests.swift in Sources */, + EE4EBD871D7FF38200E6A9CA /* OAuth2Tests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -647,32 +748,32 @@ /* Begin PBXTargetDependency section */ EE43B1561951F2440017679A /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = EEE209461942772800736F1A /* OAuth2OSX */; + target = EEE209461942772800736F1A /* OAuth2macOS */; targetProxy = EE43B1551951F2440017679A /* PBXContainerItemProxy */; }; EE43B1581951F2440017679A /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = EEE209461942772800736F1A /* OAuth2OSX */; + target = EEE209461942772800736F1A /* OAuth2macOS */; targetProxy = EE43B1571951F2440017679A /* PBXContainerItemProxy */; }; EE43B15E1951FCFB0017679A /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = EEE209461942772800736F1A /* OAuth2OSX */; + target = EEE209461942772800736F1A /* OAuth2macOS */; targetProxy = EE43B15D1951FCFB0017679A /* PBXContainerItemProxy */; }; EE43B1CC19547C4A0017679A /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = EEE209461942772800736F1A /* OAuth2OSX */; + target = EEE209461942772800736F1A /* OAuth2macOS */; targetProxy = EE43B1CB19547C4A0017679A /* PBXContainerItemProxy */; }; EE43B1CE19547C4A0017679A /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = EEE209461942772800736F1A /* OAuth2OSX */; + target = EEE209461942772800736F1A /* OAuth2macOS */; targetProxy = EE43B1CD19547C4A0017679A /* PBXContainerItemProxy */; }; EEE209AA19427DFE00736F1A /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = EEE209461942772800736F1A /* OAuth2OSX */; + target = EEE209461942772800736F1A /* OAuth2macOS */; targetProxy = EEE209A919427DFE00736F1A /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -681,6 +782,7 @@ 6598544C1C5B3BEA00237D39 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -691,12 +793,12 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; - OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT"; + OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT -DNO_MODULE_IMPORT"; PRODUCT_BUNDLE_IDENTIFIER = "org.chip.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = OAuth2; SDKROOT = appletvos; SKIP_INSTALL = YES; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 9.0; }; @@ -705,6 +807,7 @@ 6598544D1C5B3BEA00237D39 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; @@ -716,13 +819,13 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT"; + OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT -DNO_MODULE_IMPORT"; PRODUCT_BUNDLE_IDENTIFIER = "org.chip.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = OAuth2; SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 9.0; }; @@ -771,6 +874,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -811,6 +915,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.9; METAL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -823,6 +928,7 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -830,11 +936,12 @@ INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT"; + OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT -DNO_MODULE_IMPORT"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.p2.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = OAuth2; SKIP_INSTALL = YES; - SWIFT_VERSION = 2.3; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -843,6 +950,7 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -850,18 +958,19 @@ INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT"; + OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT -DNO_MODULE_IMPORT"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.p2.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = OAuth2; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Release; }; EEE2095B1942772800736F1A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -871,18 +980,19 @@ INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT"; + OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT -DNO_MODULE_IMPORT"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.p2.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = OAuth2; SDKROOT = macosx; SKIP_INSTALL = YES; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Debug; }; EEE2095C1942772800736F1A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; COMBINE_HIDPI_IMAGES = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; @@ -893,13 +1003,13 @@ INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT"; + OTHER_SWIFT_FLAGS = "-DNO_KEYCHAIN_IMPORT -DNO_MODULE_IMPORT"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.p2.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = OAuth2; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Release; }; @@ -914,10 +1024,11 @@ INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; METAL_ENABLE_DEBUG_INFO = YES; + OTHER_SWIFT_FLAGS = "-DNO_MODULE_IMPORT"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.p2.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -933,11 +1044,12 @@ INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; METAL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = "-DNO_MODULE_IMPORT"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.p2.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Release; }; @@ -971,7 +1083,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - EEE2095A1942772800736F1A /* Build configuration list for PBXNativeTarget "OAuth2OSX" */ = { + EEE2095A1942772800736F1A /* Build configuration list for PBXNativeTarget "OAuth2macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( EEE2095B1942772800736F1A /* Debug */, diff --git a/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2OSX.xcscheme b/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2macOS.xcscheme similarity index 95% rename from OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2OSX.xcscheme rename to OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2macOS.xcscheme index 7c40e5a1..34930c0b 100644 --- a/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2OSX.xcscheme +++ b/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2macOS.xcscheme @@ -16,7 +16,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "EEE209461942772800736F1A" BuildableName = "OAuth2.framework" - BlueprintName = "OAuth2OSX" + BlueprintName = "OAuth2macOS" ReferencedContainer = "container:OAuth2.xcodeproj"> @@ -58,7 +58,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "EEE209461942772800736F1A" BuildableName = "OAuth2.framework" - BlueprintName = "OAuth2OSX" + BlueprintName = "OAuth2macOS" ReferencedContainer = "container:OAuth2.xcodeproj"> @@ -80,7 +80,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "EEE209461942772800736F1A" BuildableName = "OAuth2.framework" - BlueprintName = "OAuth2OSX" + BlueprintName = "OAuth2macOS" ReferencedContainer = "container:OAuth2.xcodeproj"> @@ -98,7 +98,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "EEE209461942772800736F1A" BuildableName = "OAuth2.framework" - BlueprintName = "OAuth2OSX" + BlueprintName = "OAuth2macOS" ReferencedContainer = "container:OAuth2.xcodeproj"> diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..3b0dc2c8 --- /dev/null +++ b/Package.swift @@ -0,0 +1,36 @@ +// +// Package.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 12/19/15. +// Copyright 2015 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import PackageDescription + +let package = Package( + name: "OAuth2", + targets: [ + Target(name: "SwiftKeychain"), + Target(name: "Base", dependencies: [.Target(name: "SwiftKeychain")]), + Target(name: "macOS", dependencies: [.Target(name: "Base")]), + Target(name: "Flows", dependencies: [.Target(name: "macOS")]), + Target(name: "DataLoader", dependencies: [.Target(name: "Flows")]), + ], + dependencies: [ + // SwiftKeychain is not yet available as a Package, so we symlink to /Sources and make it a Target + //.Package(url: "https://github.com/yankodimitrov/SwiftKeychain.git", majorVersion: 1), + ] +) diff --git a/README.md b/README.md index 97851a3e..d3fec532 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,18 @@ OAuth2 [![Build Status](https://travis-ci.org/p2/OAuth2.svg?branch=master)](https://travis-ci.org/p2/OAuth2) [![License](https://img.shields.io/:license-apache-blue.svg)](LICENSE.txt) -OAuth2 frameworks for **OS X**, **iOS** and **tvOS** written in Swift 2.3. +OAuth2 frameworks for **macOS**, **iOS** and **tvOS** written in Swift 3.0. -Technical documentation is available at [p2.github.io/OAuth2](https://p2.github.io/OAuth2). -Take a look at the [OS X sample app][sample] for basic usage of this framework. +- [⤵️ Installation](#installation) +- [🛠 Usage](#usage) +- [📱 Sample iOS app](https://github.com/p2/OAuth2PodApp) (using CocoaPods) +- [🖥 Sample macOS app][sample] (with data loader examples) +- [📖 Technical Documentation](https://p2.github.io/OAuth2) -The code in this repo requires Xcode 8, the built framework can be used on **OS X 10.9** or **iOS 8** and later. -To use on **iOS 7** you'll have to include the source files in your main project. +OAuth2 requires Xcode 8, the built framework can be used on **OS X 10.9** or **iOS 8** and later. Happy to accept pull requests, please see [CONTRIBUTING.md](./CONTRIBUTING.md) -#### Swift Version +### Swift Version Since the Swift language is constantly evolving I have adopted a versioning scheme mirroring Swift versions: the framework version's **first two digits are always the Swift version** the library is compatible with, see [releases](https://github.com/p2/OAuth2/releases). @@ -25,95 +27,151 @@ Usage To use OAuth2 in your own code, start with `import OAuth2` (use `p2_OAuth2` if you installed _p2.OAuth2_ via CocoaPods) in your source files. -For a typical code grant flow you want to perform the following steps. +A typical code grant flow is used for demo purposes below. The steps for other flows are mostly the same short of instantiating a different subclass and using different client settings. -Most _authorize_ methods take an additional `params` parameter that allows you to supply custom additional parameters to use during authorization. +Still not working? +See [site-specific peculiarities](#site-specific-peculiarities). -### 1. Create a Settings Dictionary. +### 1. Instantiate OAuth2 with a Settings Dictionary + +In this example you'll be building an iOS client to Github, so the code below will be somewhere in a view controller of yours, _maybe_ the app delegate. ```swift -let settings = [ +let oauth2 = OAuth2CodeGrant(settings: [ "client_id": "my_swift_app", - "client_secret": "C7447242-A0CF-47C5-BAC7-B38BA91970A9", - "authorize_uri": "https://authorize.smarthealthit.org/authorize", - "token_uri": "https://authorize.smarthealthit.org/token", // code grant only - "scope": "profile email", - "redirect_uris": ["myapp://oauth/callback"], // register the "myapp" scheme in Info.plist - "keychain": false, // if you DON'T want keychain integration -] as OAuth2JSON + "client_secret": "C7447242", + "authorize_uri": "https://github.com/login/oauth/authorize", + "token_uri": "https://github.com/login/oauth/access_token", // code grant only + "redirect_uris": ["myapp://oauth/callback"], // register your own "myapp" scheme in Info.plist + "scope": "user repo:status", + "secret_in_body": true, // Github needs this + "keychain": false, // if you DON'T want keychain integration +] as OAuth2JSON) ``` -### 2. Instantiate OAuth2 +See those `redirect_uris`? +You can use the scheme you want, but you must **a)** declare the scheme you use in your `Info.plist` and **b)** register the very same URI on the website you connect to. -Create an `OAuth2CodeGrant` Instance. -Optionally, set the `onAuthorize` and `onFailure` closures **or** the `afterAuthorizeOrFailure` closure to keep informed about the status. +Want to avoid switching to Safari and pop up a SafariViewController or NSPanel? +Set this: ```swift -let oauth2 = OAuth2CodeGrant(settings: settings) -oauth2.onAuthorize = { parameters in - print("Did authorize with parameters: \(parameters)") -} -oauth2.onFailure = { error in // `error` is nil on cancel - if let error = error { - print("Authorization went wrong: \(error)") - } -} +oauth2.authConfig.authorizeEmbedded = true +oauth2.authConfig.authorizeContext = <# your UIViewController / NSWindow #> ``` -### 3. Authorize the User +Need to debug? Use a `.debug` or even a `.trace` logger: -By default the OS browser will be used for authorization if there is no access token present in the keychain. -To start authorization call **`authorize()`** or, to use embedded authorization, the convenience method `authorizeEmbeddedFrom(<# UIViewController or NSWindow #>)`. +```swift +oauth2.logger = OAuth2DebugLogger(.trace) +``` -The latter configures `authConfig` like so: +For more see [advanced settings](#advanced-settings) below. -- changes `authorizeEmbedded` to `true` and -- sets a root view controller/window, from which to present the login screen, as `authorizeContext`. -The login screen will only be **presented if needed** (see [_Manually Performing Authentication_](#manually-performing-authentication) below for details) and will automatically **dismiss** the login screen on success. -See [_Advanced Settings_](#advanced-settings) for other options. +### 2. Let the Data Loader or Alamofire Take Over -**Starting with iOS 9**, `SFSafariViewController` will be used when enabling embedded authorization. +Starting with version 3.0, there is an `OAuth2DataLoader` class that you can use to retrieve data from an API. +It will automatically start authorization if needed and will ensure that this works even if you have multiple calls going on. +For details on how to configure authorization see step 4 below, in this example we'll use "embedded" authorization, meaning we'll show a SFSafariViewController on iOS if the user needs to log in. -Your `oauth2` instance will use an automatically created `NSURLSession` using an `ephemeralSessionConfiguration()` configuration for its requests, exposed on `oauth2.session`. -You can set `oauth2.sessionConfiguration` to your own configuration, for example if you'd like to change timeout values. -You can also set `oauth2.sessionDelegate` to your own session delegate if you like. +[This wiki page has all you need](https://github.com/p2/OAuth2/wiki/Alamofire-4) to easily use OAuth2 with Alamofire instead. ```swift -oauth2.authConfig.authorizeEmbedded = true -oauth2.authConfig.authorizeContext = <# presenting view controller / window #> -oauth2.authorize() - -// for embedded authorization you can just use: -oauth2.authorizeEmbeddedFrom(<# presenting view controller / window #>) +let base = URL(string: "https://api.github.com")! +let url = base.appendingPathComponent("user") + +var req = oauth2.request(forURL: url) +req.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept") + +self.loader = OAuth2DataLoader(oauth2: oauth2) +loader.perform(request: req) { response in + do { + let dict = try response.responseJSON() + DispatchQueue.main.async { + // you have received `dict` JSON data! + } + } + catch let error { + DispatchQueue.main.async { + // an error occurred + } + } +} ``` -When using the OS browser or the iOS 9 Safari view controller, you will need to **intercept the callback** in your app delegate. -Let the OAuth2 instance handle the full URL: +### 3. Make Sure You Intercept the Callback + +When using the OS browser or the iOS 9+ Safari view controller, you will need to **intercept the callback** in your app delegate and let the OAuth2 instance handle the full URL: ```swift -func application(application: UIApplication, - openURL url: NSURL, - sourceApplication: String?, - annotation: AnyObject) -> Bool { - // you should probably first check if this is your URL being opened +func application(_ app: UIApplication, + open url: URL, + options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool { + // you should probably first check if this is the callback being opened if <# check #> { + // if your oauth2 instance lives somewhere else, adapt accordingly oauth2.handleRedirectURL(url) } } ``` -See [_Manually Performing Authentication_](#manually-performing-authentication) below for details on how to do this on the Mac. +You’re all set! -### 4. Receive Callback +--- -After everything completes either the `onAuthorize` or the `onFailure` closure will be called, and after that the `afterAuthorizeOrFailure` closure if it has been set. -Hence, unless you have a reason to, you don't need to set all three callbacks, you can use any of those. +If you want to dig deeper or do authorization yourself, here it goes: -### 5. Make Requests +### 4. Manually Authorize the User + +By default the OS browser will be used for authorization if there is no access token present or in the keychain. +**Starting with iOS 9**, `SFSafariViewController` will be used when enabling embedded authorization on iOS. + +To start authorization call **`authorize(params:callback:)`** or, to use embedded authorization, the convenience method `authorizeEmbedded(from:callback:)`. + +The login screen will only be **presented if needed** (see [_Manually Performing Authorization](#manually-performing-authorization) below for details) and will automatically **dismiss** the login screen on success. +See [_Advanced Settings_](#advanced-settings) for other options. -You can now obtain an `OAuth2Request`, which is an already signed `NSMutableURLRequest`, to retrieve data from your server. -This request sets the _Authorization_ header using the access token like so: `Authorization: Bearer {your access token}` + +```swift +oauth2.authorize() { authParameters, error in + if let params = authParameters { + print("Authorized! Access token is in `oauth2.accessToken`") + print("Authorized! Additional parameters: \(params)") + } + else { + print("Authorization was cancelled or went wrong: \(error)") // error will not be nil + } +} + +// for embedded authorization you can simply use: +oauth2.authorizeEmbedded(from: <# presenting view controller / window #>) { ... } + +// which is equivalent to: +oauth2.authConfig.authorizeEmbedded = true +oauth2.authConfig.authorizeContext = <# presenting view controller / window #> +oauth2.authorize() { ... } +``` + +Don't forget, when using the OS browser or the iOS 9+ Safari view controller, you will need to **intercept the callback** in your app delegate. +This is shown under step 2 above. + +See [_Manually Performing Authorization_](#manually-performing-authorization) below for details on how to do this on the Mac. + +### 5. Receive Callback + +After everything completes the callback will be called, **either** with a non-nil _authParameters_ dictionary (which may be empty!), **or** an error. +The access and refresh tokens and its expiration dates will already have been extracted and are available as `oauth2.accessToken` and `oauth2.refreshToken` parameters. +You only need to inspect the _authParameters_ dictionary if you wish to extract additional information. + +For advanced use outlined below, there is the `afterAuthorizeOrFail` block that you can use on your OAuth2 instance. +The `internalAfterAuthorizeOrFail` closure is, as its name suggests, provided for internal purposes – it is exposed for subclassing and compilation reasons and you should not mess with it. +Additionally, as of version 3.0, there are deprecated callback properties `onAuthorize` and `onFailure` that you should no longer use. + +### 6. Make Requests + +You can now obtain an `OAuth2Request`, which is an already signed `MutableURLRequest`, to retrieve data from your server. +This request sets the _Authorization_ header using the access token like so: `Authorization: Bearer {your access token}`. ```swift let req = oauth2.request(forURL: <# resource URL #>) @@ -130,22 +188,22 @@ let task = oauth2.session.dataTaskWithRequest(req) { data, response, error in task.resume() ``` -Of course you can use your own `NSURLSession` with these requests, you don't have to use `oauth2.session`. -If you use _Alamofire_ there's a [class extension below](#usage-with-alamofire) that you can use. +Of course you can use your own `URLSession` with these requests, you don't have to use `oauth2.session`; use [OAuth2DataLoader](https://github.com/p2/OAuth2/blob/master/Sources/Base/OAuth2DataLoader.swift), as shown in step 2, or hand it over to _Alamofire_. +[Here's all you need](https://github.com/p2/OAuth2/wiki/Alamofire-4) to easily use OAuth2 with Alamofire. -### 6. Cancel Authorization +### 7. Cancel Authorization You can cancel an ongoing authorization any time by calling `oauth2.abortAuthorization()`. This will cancel ongoing requests (like a code exchange request) or call the callback while you're waiting for a user to login on a webpage. The latter will dismiss embedded login screens or redirect the user back to the app. -### 7. Re-Authorize +### 8. Re-Authorize It is safe to always call `oauth2.authorize()` before performing a request. You can also perform the authorization before the first request after your app became active again. Or you can always intercept 401s in your requests and call authorize again before re-attempting the request. -### 8. Logout +### 9. Logout If you're storing tokens to the keychain, you can call `forgetTokens()` to throw them away. @@ -154,20 +212,25 @@ When using the built-in web view on iOS 8, one can use the following snippet to With the newer `SFSafariViewController`, or logins performed in the browser, it's probably best to directly **open the logout page** so the user sees the logout happen. ```swift -let storage = NSHTTPCookieStorage.sharedHTTPCookieStorage() +let storage = HTTPCookieStorage.shared storage.cookies?.forEach() { storage.deleteCookie($0) } ``` -Manually Performing Authentication ----------------------------------- +Manually Performing Authorization +--------------------------------- -The `authorize()` method will: +The `authorize(params:callback:)` method will: -1. Check if an access token that has not yet expired is in the keychain, if not -2. Check if a refresh token is in the keychain, if found -3. Try to use the refresh token to get a new access token, if it fails -4. Start the OAuth2 dance by using the `authConfig` settings to determine how to display an authorize screen to the user +1. Check if an authorize call is already running, if yes it will abort with an `OAuth2Error.alreadyAuthorizing` error +2. Check if an access token that has not yet expired is already present (or in the keychain), if not +3. Check if a refresh token is available, if found +4. Try to use the refresh token to get a new access token, if it fails +5. Start the OAuth2 dance by using the `authConfig` settings to determine how to display an authorize screen to the user + +Your `oauth2` instance will use an automatically created `URLSession` using an `ephemeralSessionConfiguration()` configuration for its requests, exposed on `oauth2.session`. +You can set `oauth2.sessionConfiguration` to your own configuration, for example if you'd like to change timeout values. +You can also set `oauth2.sessionDelegate` to your own session delegate if you like. The wiki has [the complete call graph](https://github.com/p2/OAuth2/wiki/Call-Graph) of the _authorize()_ method. If you do **not wish this kind of automation**, the manual steps to show and hide the authorize screens are: @@ -175,62 +238,57 @@ If you do **not wish this kind of automation**, the manual steps to show and hid **Embedded iOS**: ```swift -let web = oauth2.authorizeEmbeddedWith(<# presenting view controller #>) +let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>) oauth2.authConfig.authorizeEmbeddedAutoDismiss = false -oauth2.afterAuthorizeOrFailure = { wasFailure, error in +let web = try oauth2.authorizer.authorizeSafariEmbedded(from: <# view controller #>, at: url) +oauth2.afterAuthorizeOrFail = { authParameters, error in + // inspect error or oauth2.accessToken / authParameters or do something else web.dismissViewControllerAnimated(true, completion: nil) } ``` -**Modal Sheet on OS X**: - -```swift -let win = <# window to present from #> -// if `win` is nil, will open a new window -oauth2.authorizeEmbeddedFrom(win) -``` - -**Present yourself on OS X**: +**Modal Sheet on macOS**: ```swift -let vc = <# view controller #> -let web = oauth2.presentableAuthorizeViewController() -oauth2.afterAuthorizeOrFailure = { wasFailure, error in - vc.dismissViewController(web) +let window = <# window to present from #> +let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>) +let sheet = try oauth2.authorizer.authorizeEmbedded(from: window, at: url) +oauth2.afterAuthorizeOrFail = { authParameters, error in + // inspect error or oauth2.accessToken / authParameters or do something else + window.endSheet(sheet) } -vc.presentViewController(web, animator: <# animator #>) ``` -**iOS/OS X browser**: +**New window on macOS**: ```swift -try! oauth2.openAuthorizeURLInBrowser() +let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>) +let windowController = try oauth2.authorizer.authorizeInNewWindow(at: url) +oauth2.afterAuthorizeOrFail = { authParameters, error in + // inspect error or oauth2.accessToken / authParameters or do something else + windowController.window?.close() +} ``` -In case you're using the OS browser or the new Safari view controller, you will need to **intercept the callback** in your app delegate. - -**iOS** +**iOS/macOS browser**: ```swift -func application(application: UIApplication!, - openURL url: NSURL!, - sourceApplication: String!, - annotation: AnyObject!) -> Bool { - // you should probably first check if this is your URL being opened - if <# check #> { - oauth2.handleRedirectURL(url) - } +let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>) +try oauth2.authorizer.openAuthorizeURLInBrowser(url) +oauth2.afterAuthorizeOrFail = { authParameters, error in + // inspect error or oauth2.accessToken / authParameters or do something else } ``` -**OS X** + +**macOS** See the [OAuth2 Sample App][sample]'s AppDelegate class on how to receive the callback URL in your Mac app. -If the authentication displays the code to the user, e.g. with Google's `urn:ietf:wg:oauth:2.0:oob` callback URL, you can retrieve the code from the user's pasteboard and continue authorization with: +If the authorization displays the code to the user, e.g. with Google's `urn:ietf:wg:oauth:2.0:oob` callback URL, you can retrieve the code from the user's pasteboard and continue authorization with: ```swift -let pboard = NSPasteboard.generalPasteboard() -if let pasted = pboard.stringForType(NSPasteboardTypeString) { +let pboard = NSPasteboard.general() +if let pasted = pboard.string(forType: NSPasteboardTypeString) { oauth2.exchangeCodeForToken(pasted) } ``` @@ -262,7 +320,7 @@ Would be nice to add another code example here, but it's pretty much the same as #### Client Credentials -A 2-legged flow that lets an app authenticate itself via its client id and secret. +A 2-legged flow that lets an app authorize itself via its client id and secret. Instantiate `OAuth2ClientCredentials`, as usual supplying `client_id` but also a `client_secret` – plus your other configurations – in the settings dict, and you should be good to go. #### Username and Password @@ -274,58 +332,78 @@ Create an instance as shown above, set its `username` and `password` properties, Site-Specific Peculiarities --------------------------- -Some sites might not strictly adhere to the OAuth2 flow. +Some sites might not strictly adhere to the OAuth2 flow, from returning data differently like Facebook to omitting mandatory return parameters like Instagram & co. The framework deals with those deviations by creating site-specific subclasses and/or configuration details. +If you need to pass additional **headers** or **parameters**, you can supply these in the settings dict like so: + +```swift +let oauth2 = OAuth2CodeGrant(settings: [ + "client_id": "...", + ... + "headers": ["Accept": "application/vnd.github.v3+json"], + "parameters": ["duration": "permanent"], +] as OAuth2JSON) +``` - [GitHub](https://github.com/p2/OAuth2/wiki/GitHub) - [Facebook](https://github.com/p2/OAuth2/wiki/Facebook) - [Reddit](https://github.com/p2/OAuth2/wiki/Reddit) - [Google](https://github.com/p2/OAuth2/wiki/Google) - [LinkedIn](https://github.com/p2/OAuth2/wiki/LinkedIn) -- [Instagram, Bitly, ...](https://github.com/p2/OAuth2/wiki/Instagram) +- [Instagram, Bitly, Pinterest, ...](https://github.com/p2/OAuth2/wiki/Instagram,-Bitly,-Pinterest-and-others) - [Uber](https://github.com/p2/OAuth2/wiki/Uber) - [BitBucket](https://github.com/p2/OAuth2/wiki/BitBucket) -Usage with Alamofire --------------------- +Advanced Settings +----------------- -Here's an extension that can be used with Alamofire: +The main configuration you'll use with `oauth2.authConfig` is whether or not to use an embedded login: -```swift -import Alamofire - -extension OAuth2 { - public func request( - method: Alamofire.Method, - _ URLString: URLStringConvertible, - parameters: [String: AnyObject]? = nil, - encoding: Alamofire.ParameterEncoding = .URL, - headers: [String: String]? = nil) - -> Alamofire.Request - { - - var hdrs = headers ?? [:] - if let token = accessToken { - hdrs["Authorization"] = "Bearer \(token)" - } - return Alamofire.request( - method, - URLString, - parameters: parameters, - encoding: encoding, - headers: hdrs) - } -} -``` + oauth2.authConfig.authorizeEmbedded = true -You can now use the handle to your `OAuth2` instance instead of using _Alamofire_ directly to make requests that are signed. -Of course this will only work once you have an access token. -You can use `hasUnexpiredAccessToken()` to check for one or just always call `authorize()` first; it will call your callback immediately if you have a token. +Similarly, if you want to take care of dismissing the login screen yourself: -```swift -oauth2.request(.GET, "http://httpbin.org/get") -``` + oauth2.authConfig.authorizeEmbeddedAutoDismiss = false + +Some sites also want the client-id/secret combination in the request _body_, not in the _Authorization_ header: + + oauth2.authConfig.secretInBody = true + // or in your settings: + "secret_in_body": true + +Sometimes you also need to provide additional authorization parameters. +This can be done in 3 ways: + + oauth2.clientConfig.authParameters = ["duration": "permanent"] + // or in your settings: + "parameters": ["duration": "permanent"] + // or when you authorize manually: + oauth2.authorize(params: ["duration": "permanent"]) { ... } + +Similar is how you specify custom HTTP headers: + + oauth2.clientConfig.authHeaders = ["Accept": "application/json, text/plain"] + // or in your settings: + "headers": ["Accept": "application/json, text/plain"] + +Starting with version 2.0.1 on iOS 9, `SFSafariViewController` will be used for embedded authorization. +To revert to the old custom `OAuth2WebViewController`: + + oauth2.authConfig.ui.useSafariView = false + +To customize the _go back_ button when using `OAuth2WebViewController` on iOS 8 and older: + + oauth2.authConfig.ui.backButton = <# UIBarButtonItem(...) #> + + +Usage with Alamofire +-------------------- + +You'll get the best experience when using Alamofire v4 or newer and OAuth2 v3 and newer: + +- How to use Alamofire [version 4 and newer](https://github.com/p2/OAuth2/wiki/Alamofire-4) +- How to use [version 3 and older](https://github.com/p2/OAuth2/wiki/Alamofire-3) Dynamic Client Registration @@ -336,7 +414,7 @@ If during setup `registration_url` is set but `client_id` is not, the `authorize Client credentials returned from registration are stored to the keychain. The `OAuth2DynReg` class is responsible for handling client registration. -You can use its `registerClient(client:callback:)` method manually if you need to. +You can use its `register(client:callback:)` method manually if you need to. Registration parameters are taken from the client's configuration. ```swift @@ -354,7 +432,7 @@ oauth2.registerClientIfNeeded() { error in ```swift let oauth2 = OAuth2...() let dynreg = OAuth2DynReg() -dynreg.registerClient(oauth2) { params, error in +dynreg.register(client: oauth2) { params, error in if let error = error { // registration failed } @@ -368,7 +446,7 @@ dynreg.registerClient(oauth2) { params, error in Keychain -------- -This framework can transparently use the iOS and OS X 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. 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. @@ -378,40 +456,14 @@ 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-authenticate if an access token has expired but the framework has still pulled it from the keychain. +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. -Advanced Settings ------------------ - -The main configuration you'll use with `oauth2.authConfig` is whether or not to use an embedded login: - - oauth2.authConfig.authorizeEmbedded = true - -Similarly, if you want to take care of dismissing the login screen yourself: - - oauth2.authConfig.authorizeEmbeddedAutoDismiss = false - -Some sites also want the client-id/secret combination in the request _body_, not in the _Authorization_ header: - - oauth2.authConfig.secretInBody = true - -Starting with version 2.0.1 on iOS 9, `SFSafariViewController` will be used for embedded authorization. -To revert to the old custom `OAuth2WebViewController`: - - oauth2.authConfig.ui.useSafariView = false - -To customize the _go back_ button when using `OAuth2WebViewController`: - - oauth2.authConfig.ui.backButton = <# UIBarButtonItem(...) #> - - - Installation ------------ -You can use _git_, _CocoaPods_ and possibly _Carthage_ to install the framework. +You can use _git_, _CocoaPods_ or _Carthage_ to install the framework. #### CocoaPods @@ -420,16 +472,16 @@ If you're unfamiliar with CocoaPods, read [using CocoaPods](http://guides.cocoap ```ruby platform :ios, '8.0' # or platform :osx, '10.9' -pod 'p2.OAuth2' +pod 'p2.OAuth2', '~> 3.0' use_frameworks! ``` #### Carthage -Install via Carthage is possibly working with this Cartfile: +Install via Carthage is easy enough: ```ruby -github "p2/OAuth2" ~> 2.2 +github "p2/OAuth2" ~> 3.0 ``` #### git @@ -454,10 +506,6 @@ These three steps are needed to: 2. Link the framework into your app 3. Embed the framework in your app when distributing -> NOTE that as of Xcode 6.2, the "embed" step happens in the "General" tab. -> You may want to perform step 2 and 3 from the "General" tab. -> Also make sure you select the framework for the platform, as of Xcode 7 this is visible behind _OAuth2.framework_. - License ------- diff --git a/Sources/Base/OAuth2.swift b/Sources/Base/OAuth2.swift deleted file mode 100644 index 6ac47fb0..00000000 --- a/Sources/Base/OAuth2.swift +++ /dev/null @@ -1,745 +0,0 @@ -// -// OAuth2.swift -// OAuth2 -// -// Created by Pascal Pfiffner on 6/4/14. -// Copyright 2014 Pascal Pfiffner -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - - -/** - Base class for specific OAuth2 authentication flow implementations. - */ -public class OAuth2: OAuth2Base { - - /// The grant type represented by the class, e.g. "authorization_code" for code grants. - public class var grantType: String { - return "__undefined" - } - - /// The response type expected from an authorize call, e.g. "code" for code grants. - public class var responseType: String? { - return nil - } - - /// Settings related to the client-server relationship. - public let clientConfig: OAuth2ClientConfig - - /// Client-side authorization options. - public var authConfig = OAuth2AuthConfig() - - /// The client id. - public final var clientId: String? { - get { return clientConfig.clientId } - set { clientConfig.clientId = newValue } - } - - /// The client secret, usually only needed for code grant. - public final var clientSecret: String? { - get { return clientConfig.clientSecret } - set { clientConfig.clientSecret = newValue } - } - - /// The name of the client, as used during dynamic client registration. Use "client_name" during initalization to set. - public var clientName: String? { - get { return clientConfig.clientName } - } - - /// The URL to authorize against. - public final var authURL: NSURL { - get { return clientConfig.authorizeURL } - } - - /// The URL string where we can exchange a code for a token; if nil `authURL` will be used. - public final var tokenURL: NSURL? { - get { return clientConfig.tokenURL } - } - - /// The scope currently in use. - public var scope: String? { - get { return clientConfig.scope } - set { clientConfig.scope = newValue } - } - - /// The redirect URL string to use. - public var redirect: String? { - get { return clientConfig.redirect } - set { clientConfig.redirect = newValue } - } - - /// Context for the current auth dance. - var context = OAuth2ContextStore() - - /// The receiver's access token. - public var accessToken: String? { - get { return clientConfig.accessToken } - set { clientConfig.accessToken = newValue } - } - - /// The receiver's id token. - public var idToken: String? { - get { return clientConfig.idToken } - set { clientConfig.idToken = newValue } - } - - /// The access token's expiry date. - public var accessTokenExpiry: NSDate? { - get { return clientConfig.accessTokenExpiry } - set { clientConfig.accessTokenExpiry = newValue } - } - - /// The receiver's long-time refresh token. - public var refreshToken: String? { - get { return clientConfig.refreshToken } - set { clientConfig.refreshToken = newValue } - } - - /// Contains parameters header. - public var authHeaders: OAuth2Headers? { - get { return clientConfig.authHeaders } - set { clientConfig.authHeaders = newValue } - } - - /// Closure called on successful authentication on the main thread. - public final var onAuthorize: ((parameters: OAuth2JSON) -> Void)? - - /// When authorization fails (if error is not nil) or is cancelled, this block is executed on the main thread. - public final var onFailure: ((error: ErrorType?) -> Void)? - - /** - Closure called after onAuthorize OR onFailure, on the main thread; useful for cleanup operations. - - - parameter wasFailure: Bool indicating success or failure - - parameter error: ErrorType describing the reason for failure, as supplied to the `onFailure` callback. If it is nil and `wasFailure` - is true, the process was aborted. - */ - public final var afterAuthorizeOrFailure: ((wasFailure: Bool, error: ErrorType?) -> Void)? - - /// Same as `afterAuthorizeOrFailure`, but only for internal use and called right BEFORE the public variant. - final var internalAfterAuthorizeOrFailure: ((wasFailure: Bool, error: ErrorType?) -> Void)? - - /// If non-nil, will be called before performing dynamic client registration, giving you a chance to instantiate your own registrar. - public final var onBeforeDynamicClientRegistration: (NSURL -> OAuth2DynReg?)? - - - /** - Designated initializer. - - The following settings keys are currently supported: - - - client_id (string) - - 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 - - redirect_uris (list of URL-strings) - - scope (string) - - - client_name (string) - - registration_uri (URL-string) - - logo_uri (URL-string) - - - keychain (bool, true by default, applies to using the system keychain) - - keychain_access_mode (string, value for keychain kSecAttrAccessible attribute, kSecAttrAccessibleWhenUnlocked by default) - - verbose (bool, false by default, applies to client logging) - - secret_in_body (bool, false by default, forces the flow to use the request body for the client secret) - - token_assume_unexpired (bool, true by default, whether to use access tokens that do not come with an "expires_in" parameter) - - title (string, to be shown in views shown by the framework) - */ - public override init(settings: OAuth2JSON) { - clientConfig = OAuth2ClientConfig(settings: settings) - - // auth configuration options - if let inBody = settings["secret_in_body"] as? Bool { - authConfig.secretInBody = inBody - } - if let ttl = settings["title"] as? String { - authConfig.ui.title = ttl - } - super.init(settings: settings) - } - - - // MARK: - Keychain Integration - - /** Overrides base implementation to return the authorize URL. */ - public override func keychainServiceName() -> String { - return authURL.description - } - - override func updateFromKeychainItems(items: [String : NSCoding]) { - for message in clientConfig.updateFromStorableItems(items) { - logger?.debug("OAuth2", msg: message) - } - authConfig.secretInBody = (clientConfig.endpointAuthMethod == OAuth2EndpointAuthMethod.ClientSecretPost) - } - - public override func storableCredentialItems() -> [String : NSCoding]? { - return clientConfig.storableCredentialItems() - } - - public override func storableTokenItems() -> [String : NSCoding]? { - return clientConfig.storableTokenItems() - } - - public override func forgetClient() { - super.forgetClient() - clientConfig.forgetCredentials() - } - - public override func forgetTokens() { - super.forgetTokens() - clientConfig.forgetTokens() - } - - - // MARK: - Authorization - - /** - Use this method, together with `authConfig`, to obtain an access token. - - This method will first check if the client already has an unexpired access token (possibly from the keychain), if not and it's able to - use a refresh token it will try to use the refresh token. If this fails it will check whether the client has a client_id and show the - authorize screen if you have `authConfig` set up sufficiently. If `authConfig` is not set up sufficiently this method will end up - calling the `onFailure` callback. If client_id is not set but a "registration_uri" has been provided, a dynamic client registration will - be attempted and if it succees, an access token will be requested. - - - parameter params: Optional key/value pairs to pass during authorization and token refresh - */ - public final func authorize(params params: OAuth2StringDict? = nil) { - isAuthorizing = true - tryToObtainAccessTokenIfNeeded(params: params) { success in - if success { - self.didAuthorize(OAuth2JSON()) - } - else { - self.registerClientIfNeeded() { json, error in - if let error = error { - self.didFail(error) - } - else { - do { - assert(NSThread.isMainThread()) - try self.doAuthorize(params: params) - } - catch let error { - self.didFail(error) - } - } - } - } - } - } - - /** - Shortcut function to start embedded authorization from the given context (a UIViewController on iOS, an NSWindow on OS X). - - This method sets `authConfig.authorizeEmbedded = true` and `authConfig.authorizeContext = <# context #>`, then calls `authorize()` - - - parameter context: The context to start authorization from, depends on platform (UIViewController or NSWindow, see `authorizeContext`) - - parameter params: Optional key/value pairs to pass during authorization - */ - public func authorizeEmbeddedFrom(context: AnyObject, params: OAuth2StringDict? = nil) { - authConfig.authorizeEmbedded = true - authConfig.authorizeContext = context - authorize(params: params) - } - - /** - If the instance has an accessToken, checks if its expiry time has not yet passed. If we don't have an expiry date we assume the token - is still valid. - - - returns: A Bool indicating whether a probably valid access token exists - */ - public func hasUnexpiredAccessToken() -> Bool { - if let access = accessToken where !access.isEmpty { - if let expiry = accessTokenExpiry { - return expiry == expiry.laterDate(NSDate()) - } - return clientConfig.accessTokenAssumeUnexpired - } - return false - } - - /** - Attempts to receive a new access token by: - - 1. checking if there still is an unexpired token - 2. attempting to use a refresh token - - Indicates, in the callback, whether the client has been able to obtain an access token that is likely to still work (but there is no - guarantee!) or not. - - - parameter params: Optional key/value pairs to pass during authorization - - parameter callback: The callback to call once the client knows whether it has an access token or not; if `success` is true an - access token is present - */ - public func tryToObtainAccessTokenIfNeeded(params params: OAuth2StringDict? = nil, callback: ((success: Bool) -> Void)) { - if hasUnexpiredAccessToken() { - callback(success: true) - } - else { - logger?.debug("OAuth2", msg: "No access token, maybe I can refresh") - doRefreshToken(params: params) { successParams, error in - if nil != successParams { - callback(success: true) - } - else { - if let err = error { - self.logger?.debug("OAuth2", msg: "\(err)") - } - callback(success: false) - } - } - } - } - - /** - Method to actually start authorization. The public `authorize()` method only proceeds to this method if there is no valid access token - and if optional client registration succeeds. - - Can be overridden in subclasses to perform an authorization dance different from directing the user to a website. - - - parameter params: Optional key/value pairs to pass during authorization - */ - public func doAuthorize(params params: OAuth2StringDict? = nil) throws { - if self.authConfig.authorizeEmbedded { - try self.authorizeEmbeddedWith(self.authConfig, params: params) - } - else { - try self.openAuthorizeURLInBrowser(params) - } - } - - /** - Method that creates the OAuth2AuthRequest instance used to create the authorize URL - - - parameter redirect: The redirect URI string to supply. If it is nil, the first value of the settings' `redirect_uris` entries is - used. Must be present in the end! - - parameter scope: The scope to request - - parameter params: Any additional parameters as dictionary with string keys and values that will be added to the query part - - returns: OAuth2AuthRequest to be used to call to the authorize endpoint - */ - func authorizeRequestWithRedirect(redirect: String, scope: String?, params: OAuth2StringDict?) throws -> OAuth2AuthRequest { - guard let clientId = clientConfig.clientId where !clientId.isEmpty else { - throw OAuth2Error.NoClientId - } - - let req = OAuth2AuthRequest(url: clientConfig.authorizeURL, method: .GET) - req.params["redirect_uri"] = redirect - req.params["client_id"] = clientId - req.params["state"] = context.state - if let scope = scope ?? clientConfig.scope { - req.params["scope"] = scope - } - if let responseType = self.dynamicType.responseType { - req.params["response_type"] = responseType - } - req.addParams(params: params) - - return req - } - - /** - Most convenient method if you want the authorize URL to be created as defined in your settings dictionary. - - - parameter params: Optional, additional URL params to supply to the request - - returns: NSURL to be used to start the OAuth dance - */ - public func authorizeURL(params: OAuth2StringDict? = nil) throws -> NSURL { - return try authorizeURLWithRedirect(nil, scope: nil, params: params) - } - - /** - Convenience method to be overridden by and used from subclasses. - - - parameter redirect: The redirect URI string to supply. If it is nil, the first value of the settings' `redirect_uris` entries is - used. Must be present in the end! - - parameter scope: The scope to request - - parameter params: Any additional parameters as dictionary with string keys and values that will be added to the query part - - returns: NSURL to be used to start the OAuth dance - */ - public func authorizeURLWithRedirect(redirect: String?, scope: String?, params: OAuth2StringDict?) throws -> NSURL { - guard let redirect = (redirect ?? clientConfig.redirect) else { - throw OAuth2Error.NoRedirectURL - } - let req = try authorizeRequestWithRedirect(redirect, scope: scope, params: params) - context.redirectURL = redirect - return try req.asURL() - } - - /** - Subclasses override this method to extract information from the supplied redirect URL. - - - parameter redirect: The redirect URL returned by the server that you want to handle - */ - public func handleRedirectURL(redirect: NSURL) throws { - throw OAuth2Error.Generic("Abstract class use") - } - - - // MARK: - Refresh Token - - /** - Generate the request to be used for token refresh when we have a refresh token. - - This will set "grant_type" to "refresh_token", add the refresh token, and take care of the remaining parameters. - - - parameter params: Additional parameters to pass during token refresh - - returns: An `OAuth2AuthRequest` instance that is configured for token refresh - */ - func tokenRequestForTokenRefresh(params params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { - guard let clientId = clientId where !clientId.isEmpty else { - throw OAuth2Error.NoClientId - } - guard let refreshToken = clientConfig.refreshToken where !refreshToken.isEmpty else { - throw OAuth2Error.NoRefreshToken - } - - let req = OAuth2AuthRequest(url: (clientConfig.tokenURL ?? clientConfig.authorizeURL)) - req.params["grant_type"] = "refresh_token" - req.params["refresh_token"] = refreshToken - req.params["client_id"] = clientId - req.addParams(params: params) - - return req - } - - /** - If there is a refresh token, use it to receive a fresh access token. - - - parameter params: Optional key/value pairs to pass during token refresh - - parameter callback: The callback to call after the refresh token exchange has finished - */ - public func doRefreshToken(params params: OAuth2StringDict? = nil, callback: ((successParams: OAuth2JSON?, error: ErrorType?) -> Void)) { - do { - let post = try tokenRequestForTokenRefresh(params: params).asURLRequestFor(self) - logger?.debug("OAuth2", msg: "Using refresh token to receive access token from \(post.URL?.description ?? "nil")") - - performRequest(post) { data, status, error in - do { - guard let data = data else { - throw error ?? OAuth2Error.NoDataInResponse - } - let json = try self.parseRefreshTokenResponseData(data) - if status < 400 { - self.logger?.debug("OAuth2", msg: "Did use refresh token for access token [\(nil != self.clientConfig.accessToken)]") - if self.useKeychain { - self.storeTokensToKeychain() - } - callback(successParams: json, error: nil) - } - else { - throw OAuth2Error.Generic("\(status)") - } - } - catch let error { - self.logger?.debug("OAuth2", msg: "Error parsing refreshed access token: \(error)") - callback(successParams: nil, error: error) - } - } - } - catch let error { - callback(successParams: nil, error: error) - } - } - - - // MARK: - Registration - - /** - Use OAuth2 dynamic client registration to register the client, if needed. - - Returns immediately if the receiver's `clientId` is nil (with error = nil) or if there is no registration URL (with error). Otherwise - calls `onBeforeDynamicClientRegistration()` -- if it is non-nil -- and uses the returned `OAuth2DynReg` instance -- if it is non-nil. - If both are nil, instantiates a blank `OAuth2DynReg` instead, then attempts client registration. - - - 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: ((json: OAuth2JSON?, error: ErrorType?) -> Void)) { - if nil != clientId { - callOnMainThread() { - callback(json: nil, error: nil) - } - } - else if let url = clientConfig.registrationURL { - let dynreg = onBeforeDynamicClientRegistration?(url) ?? OAuth2DynReg() - dynreg.registerClient(self) { json, error in - callOnMainThread() { - callback(json: json, error: error) - } - } - } - else { - callOnMainThread() { - callback(json: nil, error: OAuth2Error.NoRegistrationURL) - } - } - } - - - // MARK: - Callbacks - - /// Flag used internally to determine whether authorization is going on at all and can be aborted. - private var isAuthorizing = false - - /** - Internally used on success. Calls the `onAuthorize` and `afterAuthorizeOrFailure` callbacks on the main thread. - - This method is only made public in case you want to create a subclass and call `didAuthorize(parameters:)` at an override point. If you - call this method yourself on standard classes you might screw things up badly. - - - parameter parameters: The parameters received during authorization - */ - public final func didAuthorize(parameters: OAuth2JSON) { - isAuthorizing = false - if useKeychain { - storeTokensToKeychain() - } - callOnMainThread() { - self.onAuthorize?(parameters: parameters) - self.internalAfterAuthorizeOrFailure?(wasFailure: false, error: nil) - self.afterAuthorizeOrFailure?(wasFailure: false, error: nil) - } - } - - /** - Internally used on error. Calls the `onFailure` and `afterAuthorizeOrFailure` callbacks on the main thread. - - This method is only made public in case you want to create a subclass and need to call `didFail(error:)` at an override point. If you - call this method yourself on standard classes you might screw things up royally. - - - parameter error: The error that led to authentication failure - */ - public final func didFail(error: ErrorType?) { - isAuthorizing = false - - var finalError = error - if let error = error { - logger?.debug("OAuth2", msg: "\(error)") - if let oae = error as? OAuth2Error where .RequestCancelled == oae { - finalError = nil - } - } - - callOnMainThread() { - self.onFailure?(error: finalError) - self.internalAfterAuthorizeOrFailure?(wasFailure: true, error: error) - self.afterAuthorizeOrFailure?(wasFailure: true, error: error) - } - } - - - // MARK: - Requests - - /** - Return an OAuth2Request, a NSMutableURLRequest subclass, that has already been signed and can be used against your OAuth2 endpoint. - - This method by default ignores locally cached data and specifies a timeout interval of 20 seconds. This should be ideal for small JSON - data requests, but you probably don't want to disable cache for binary data like avatars. - - - parameter forURL: The URL to create a request for - - parameter cachePolicy: The cache policy to use, defaults to `NSURLRequestCachePolicy.ReloadIgnoringLocalCacheData` - - returns: OAuth2Request for the given URL - */ - public func request(forURL url: NSURL, cachePolicy: NSURLRequestCachePolicy = .ReloadIgnoringLocalCacheData) -> OAuth2Request { - return OAuth2Request(URL: url, oauth: self, cachePolicy: cachePolicy, timeoutInterval: 20) - } - - /** - Allows to abort authorization currently in progress. - */ - public func abortAuthorization() { - if !abortTask() && isAuthorizing { - logger?.debug("OAuth2", msg: "Aborting authorization") - didFail(nil) - } - } - - - // MARK: - Response Parsing - - /** - Parse response data returned while exchanging the code for a token. - - This method expects token data to be JSON, decodes JSON and fills the receiver's properties accordingly. If the response contains an - "error" key, will parse the error and throw it. - - - parameter data: NSData returned from the call - - returns: An OAuth2JSON instance with token data; may contain additional information - */ - public func parseAccessTokenResponseData(data: NSData) throws -> OAuth2JSON { - let dict = try parseJSON(data) - return try parseAccessTokenResponse(dict) - } - - /** - Parse response data returned while exchanging the code for a token. - - This method extracts token data and fills the receiver's properties accordingly. If the response contains an "error" key, will parse the - error and throw it. The method is final to ensure correct order of error parsing and not parsing the response if an error happens. This - is not possible in overrides. Instead, override the various `assureXy(dict:)` methods, especially `assureAccessTokenParamsAreValid()`. - - - parameter params: Dictionary data parsed from the response - - returns: An OAuth2JSON instance with token data; may contain additional information - */ - final func parseAccessTokenResponse(params: OAuth2JSON) throws -> OAuth2JSON { - try assureNoErrorInResponse(params) - try assureCorrectBearerType(params) - try assureAccessTokenParamsAreValid(params) - - clientConfig.updateFromResponse(normalizeAccessTokenResponseKeys(params)) - return params - } - - /** - This method does nothing, but allows subclasses to fix parameter names before passing the access token response to `OAuth2ClientConfig`s - `updateFromResponse()`. - - - parameter dict: The dictionary that was returned from an access token response - - returns: The dictionary with fixed key names - */ - public func normalizeAccessTokenResponseKeys(dict: OAuth2JSON) -> OAuth2JSON { - return dict - } - - /** - Parse response data returned while using a refresh token. - - This method extracts token data, expected to be JSON, and fills the receiver's properties accordingly. If the response contains an - "error" key, will parse the error and throw it. - - - parameter data: NSData returned from the call - - returns: An OAuth2JSON instance with token data; may contain additional information - */ - public func parseRefreshTokenResponseData(data: NSData) throws -> OAuth2JSON { - let dict = try parseJSON(data) - return try parseRefreshTokenResponse(dict) - } - - /** - Parse response data returned while using a refresh token. - - This method extracts token data and fills the receiver's properties accordingly. If the response contains an "error" key, will parse the - error and throw it. The method is final to ensure correct order of error parsing and not parsing the response if an error happens. This - is not possible in overrides. Instead, override the various `assureXy(dict:)` methods, especially `assureRefreshTokenParamsAreValid()`. - - - parameter json: Dictionary data parsed from the response - - returns: An OAuth2JSON instance with token data; may contain additional information - */ - final func parseRefreshTokenResponse(dict: OAuth2JSON) throws -> OAuth2JSON { - try assureNoErrorInResponse(dict) - try assureCorrectBearerType(dict) - try assureRefreshTokenParamsAreValid(dict) - - clientConfig.updateFromResponse(dict) - return dict - } - - /** - This method does nothing, but allows subclasses to fix parameter names before passing the refresh token response to - `OAuth2ClientConfig`s `updateFromResponse()`. - - - parameter dict: The dictionary that was returned from a refresh token response - - returns: The dictionary with fixed key names - */ - public func normalizeRefreshTokenResponseKeys(dict: OAuth2JSON) -> OAuth2JSON { - return dict - } - - /** - This method checks `state`, throws `OAuth2Error.InvalidState` if it doesn't match. Resets state if it matches. - */ - func assureMatchesState(params: OAuth2JSON) throws { - if !context.matchesState(params["state"] as? String) { - throw OAuth2Error.InvalidState - } - context.resetState() - } - - /** - Throws unless "token_type" is "bearer" (case-insensitive). - */ - func assureCorrectBearerType(params: OAuth2JSON) throws { - if let tokType = params["token_type"] as? String { - if "bearer" == tokType.lowercaseString { - return - } - throw OAuth2Error.UnsupportedTokenType("Only “bearer” token is supported, but received “\(tokType)”") - } - throw OAuth2Error.NoTokenType - } - - /** - Called when parsing the access token response. Does nothing by default, implicit grant flows check state. - */ - public func assureAccessTokenParamsAreValid(params: OAuth2JSON) throws { - } - - /** - Called when parsing the refresh token response. Does nothing by default. - */ - public func assureRefreshTokenParamsAreValid(params: OAuth2JSON) throws { - } -} - - -/** -Class, internally used, to store current authorization context, such as state and redirect-url. -*/ -class OAuth2ContextStore { - - /// Currently used redirect_url. - var redirectURL: String? - - /// The current state. - internal(set) var _state = "" - - /** - The state sent to the server when requesting a token. - - We internally generate a UUID and use the first 8 chars if `_state` is empty. - */ - var state: String { - if _state.isEmpty { - _state = NSUUID().UUIDString - _state = _state[_state.startIndex..<_state.startIndex.advancedBy(8)] // only use the first 8 chars, should be enough - } - return _state - } - - /** - Checks that given state matches the internal state. - - - parameter state: The state to check (may be nil) - - returns: true if state matches, false otherwise or if given state is nil. - */ - func matchesState(state: String?) -> Bool { - if let st = state { - return st == _state - } - return false - } - - /** - Resets current state so it gets regenerated next time it's needed. - */ - func resetState() { - _state = "" - } -} - diff --git a/Sources/Base/OAuth2AuthConfig.swift b/Sources/Base/OAuth2AuthConfig.swift index 1cfe1a26..33bdac3f 100644 --- a/Sources/Base/OAuth2AuthConfig.swift +++ b/Sources/Base/OAuth2AuthConfig.swift @@ -18,9 +18,6 @@ // limitations under the License. // -#if os(OSX) -import Cocoa -#endif /** Simple struct to hold settings describing how authorization appears to the user. @@ -33,21 +30,11 @@ public struct OAuth2AuthConfig { /// Title to propagate to views handled by OAuth2, such as OAuth2WebViewController. public var title: String? = nil - // TODO: figure out a neat way to make this a UIBarButtonItem if compiled for iOS /// By assigning your own UIBarButtonItem (!) you can override the back button that is shown in the iOS embedded web view (does NOT apply to `SFSafariViewController`). public var backButton: AnyObject? = nil /// 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 - - #if os(OSX) - /// Internally used to store default `NSWindowController` created to contain the web view controller. - var windowController: NSWindowController? - - #elseif os(iOS) - /// Internally used to store the `SFSafariViewControllerDelegate`. - var safariViewDelegate: AnyObject? - #endif } /// Whether the receiver should use the request body instead of the Authorization header for the client secret; defaults to `false`. @@ -60,9 +47,8 @@ public struct OAuth2AuthConfig { public var authorizeEmbeddedAutoDismiss = true /// Context information for the authorization flow: - /// - iOS: the parent view controller to present from - /// - OS X: An NSWindow from which to present a modal sheet _or_ - /// - OS X: A `((webViewController: NSViewController) -> Void)` block to execute with the web view controller for you to present + /// - 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 /// UI-specific configuration. diff --git a/Sources/Base/OAuth2AuthRequest.swift b/Sources/Base/OAuth2AuthRequest.swift index fc2c47a6..dae86dc3 100644 --- a/Sources/Base/OAuth2AuthRequest.swift +++ b/Sources/Base/OAuth2AuthRequest.swift @@ -36,42 +36,79 @@ Content types that will be specified in the request header under "Content-type". public enum OAuth2HTTPContentType: String { /// JSON content: `application/json` - case JSON = "application/json" + case json = "application/json" /// Form encoded content, using UTF-8: `application/x-www-form-urlencoded; charset=utf-8` - case WWWForm = "application/x-www-form-urlencoded; charset=utf-8" + case wwwForm = "application/x-www-form-urlencoded; charset=utf-8" +} + + +/** +The auth method supported by the endpoint. +*/ +public enum OAuth2EndpointAuthMethod: String { + case none = "none" + case clientSecretPost = "client_secret_post" + case clientSecretBasic = "client_secret_basic" } /** Class representing an OAuth2 authorization request that can be used to create NSURLRequest instances. */ -public class OAuth2AuthRequest { +open class OAuth2AuthRequest { /// The url of the receiver. Queries may by added by parameters specified on `params`. - public let url: NSURL + open let url: URL /// The HTTP method. - public let method: OAuth2HTTPMethod + open let method: OAuth2HTTPMethod - /// The content type that will be specified. Defaults to `WWWForm`. - public var contentType = OAuth2HTTPContentType.WWWForm + /// The content type that will be specified. Defaults to `wwwForm`. + open var contentType = OAuth2HTTPContentType.wwwForm - /// If set will take preference over any "Authorize" header that would otherwise be set. - public var headerAuthorize: String? + /// Custom headers can be set here, they will take precedence over any built-in headers. + open private(set) var headers: [String: String]? - public var params = OAuth2AuthRequestParams() + open var params = OAuth2RequestParams() /** Designated initializer. Neither URL nor method can later be changed. */ - public init(url: NSURL, method: OAuth2HTTPMethod = .POST) { + public init(url: URL, method: OAuth2HTTPMethod = .POST) { self.url = url self.method = method } + // MARK: - Headers + + /** + Set the given custom header. + + - parameter header: The header's name + - parameter value: The value to use + */ + public func set(header: String, to value: String) { + if nil == headers { + headers = [header: value] + } + else { + headers![header] = value + } + } + + /** + Unset the given header so that the default can be applied again. + + - parameter header: The header's name + */ + public func unset(header: String) { + _ = headers?.removeValue(forKey: header) + } + + // MARK: - Parameter /** @@ -79,7 +116,7 @@ public class OAuth2AuthRequest { - parameter params: The parameters to add to the receiver */ - public func addParams(params inParams: OAuth2StringDict?) { + open func add(params inParams: OAuth2StringDict?) { if let prms = inParams { for (key, val) in prms { params[key] = val @@ -95,10 +132,10 @@ public class OAuth2AuthRequest { - returns: NSURLComponents representing the receiver */ - func asURLComponents() throws -> NSURLComponents { - let comp = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) - guard let components = comp where "https" == components.scheme else { - throw OAuth2Error.NotUsingTLS + func asURLComponents() throws -> URLComponents { + let comp = URLComponents(url: url, resolvingAgainstBaseURL: false) + guard var components = comp, "https" == components.scheme else { + throw OAuth2Error.notUsingTLS } if .GET == method && params.count > 0 { components.percentEncodedQuery = params.percentEncodedQueryString() @@ -111,12 +148,12 @@ public class OAuth2AuthRequest { - returns: An NSURL representing the receiver */ - public func asURL() throws -> NSURL { + open func asURL() throws -> URL { let comp = try asURLComponents() - if let finalURL = comp.URL { + if let finalURL = comp.url { return finalURL } - throw OAuth2Error.InvalidURLComponents(comp) + throw OAuth2Error.invalidURLComponents(comp) } /** @@ -125,26 +162,18 @@ public class OAuth2AuthRequest { - parameter oauth2: The OAuth2 instance from which to take client and auth settings - returns: A mutable NSURLRequest */ - public func asURLRequestFor(oauth2: OAuth2) throws -> NSMutableURLRequest { + open func asURLRequest(for oauth2: OAuth2Base) throws -> URLRequest { var finalParams = params - var finalAuthHeader = headerAuthorize // base request let finalURL = try asURL() - let req = NSMutableURLRequest(URL: finalURL) - req.HTTPMethod = method.rawValue + var req = URLRequest(url: finalURL) + req.httpMethod = method.rawValue req.setValue(contentType.rawValue, forHTTPHeaderField: "Content-Type") req.setValue("application/json", forHTTPHeaderField: "Accept") - // add custom headers - if let headerParams = oauth2.authHeaders where !headerParams.isEmpty { - for (key, value) in headerParams { - req.setValue(value, forHTTPHeaderField: key) - } - } - // handle client secret if there is one - if let clientId = oauth2.clientConfig.clientId where !clientId.isEmpty, let secret = oauth2.clientConfig.clientSecret { + if let clientId = oauth2.clientConfig.clientId, !clientId.isEmpty, let secret = oauth2.clientConfig.clientSecret { // add to request body if oauth2.authConfig.secretInBody { @@ -154,28 +183,37 @@ public class OAuth2AuthRequest { } // add Authorization header (if not in body) - else if nil == finalAuthHeader { + else { oauth2.logger?.debug("OAuth2", msg: "Adding “Authorization” header as “Basic client-key:client-secret”") let pw = "\(clientId.wwwFormURLEncodedString):\(secret.wwwFormURLEncodedString)" - if let utf8 = pw.dataUsingEncoding(NSUTF8StringEncoding) { - finalAuthHeader = "Basic \(utf8.base64EncodedStringWithOptions([]))" + if let utf8 = pw.data(using: String.Encoding.utf8) { + req.setValue("Basic \(utf8.base64EncodedString())", forHTTPHeaderField: "Authorization") } else { - throw OAuth2Error.UTF8EncodeError + throw OAuth2Error.utf8EncodeError } - finalParams.removeValueForKey("client_id") - finalParams.removeValueForKey("client_secret") + finalParams.removeValue(forKey: "client_id") + finalParams.removeValue(forKey: "client_secret") } } - // add custom Authorize header - if let authHeader = finalAuthHeader { - req.setValue(authHeader, forHTTPHeaderField: "Authorization") + // add custom headers, first from our OAuth2 instance, then our custom ones + if let headers = oauth2.authHeaders { + for (key, val) in headers { + oauth2.logger?.trace("OAuth2", msg: "Overriding “\(key)” header") + req.setValue(val, forHTTPHeaderField: key) + } + } + if let headers = headers { + for (key, val) in headers { + oauth2.logger?.trace("OAuth2", msg: "Adding custom “\(key)” header") + req.setValue(val, forHTTPHeaderField: key) + } } // add a body to POST requests if .POST == method && finalParams.count > 0 { - req.HTTPBody = try finalParams.utf8EncodedData() + req.httpBody = try finalParams.utf8EncodedData() } return req } @@ -186,10 +224,10 @@ public class OAuth2AuthRequest { Struct to hold on to request parameters. Provides utility functions so the parameters can be correctly encoded for use in URLs and request bodies. */ -public struct OAuth2AuthRequestParams { +public struct OAuth2RequestParams { /// The parameters to be used. - private var params: OAuth2StringDict? = nil + public private(set) var params: OAuth2StringDict? = nil public init() { } @@ -206,11 +244,12 @@ public struct OAuth2AuthRequestParams { /** Removes the given value from the receiver, if it is defined. - - parameter key: The key for the value to be removed + - parameter forKey: The key for the value to be removed - returns: The value that was removed, if any */ - public mutating func removeValueForKey(key: String) -> String? { - return params?.removeValueForKey(key) + @discardableResult + public mutating func removeValue(forKey key: String) -> String? { + return params?.removeValue(forKey: key) } /// The number of items in the receiver. @@ -226,16 +265,16 @@ public struct OAuth2AuthRequestParams { - returns: NSData representing the receiver form-encoded */ - public func utf8EncodedData() throws -> NSData? { + public func utf8EncodedData() throws -> Data? { guard nil != params else { return nil } let body = percentEncodedQueryString() - if let encoded = body.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) { + if let encoded = body.data(using: String.Encoding.utf8, allowLossyConversion: true) { return encoded } else { - throw OAuth2Error.UTF8EncodeError + throw OAuth2Error.utf8EncodeError } } @@ -248,7 +287,7 @@ public struct OAuth2AuthRequestParams { guard let params = params else { return "" } - return self.dynamicType.formEncodedQueryStringFor(params) + return type(of: self).formEncodedQueryStringFor(params) } /** @@ -260,12 +299,12 @@ public struct OAuth2AuthRequestParams { - parameter params: The parameters you want to have encoded - returns: An URL-ready query string */ - public static func formEncodedQueryStringFor(params: OAuth2StringDict) -> String { + public static func formEncodedQueryStringFor(_ params: OAuth2StringDict) -> String { var arr: [String] = [] for (key, val) in params { arr.append("\(key)=\(val.wwwFormURLEncodedString)") } - return arr.joinWithSeparator("&") + return arr.joined(separator: "&") } } diff --git a/Sources/Base/OAuth2AuthorizerUI.swift b/Sources/Base/OAuth2AuthorizerUI.swift new file mode 100644 index 00000000..790bccbe --- /dev/null +++ b/Sources/Base/OAuth2AuthorizerUI.swift @@ -0,0 +1,48 @@ +// +// OAuth2AuthorizerUI.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 21/07/16. +// Copyright 2016 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +/** +Platform-dependent authorizers must adopt this protocol. +*/ +public protocol OAuth2AuthorizerUI { + + /// The OAuth2 instance this authorizer belongs to. + unowned var oauth2: OAuth2Base { get } + + /** + Open the authorize URL in the OS browser. + + - parameter url: The authorize URL to open + - throws: UnableToOpenAuthorizeURL on failure + */ + func openAuthorizeURLInBrowser(_ url: URL) throws + + /** + Tries to use the given context to present the authorization screen. Context could be a UIViewController for iOS or an NSWindow on macOS. + + - parameter with: The configuration to be used; usually uses the instance's `authConfig` + - parameter at: The authorize URL to open + - throws: Can throw OAuth2Error if the method is unable to show the authorize screen + */ + func authorizeEmbedded(with config: OAuth2AuthConfig, at url: URL) throws +} diff --git a/Sources/Base/OAuth2Base.swift b/Sources/Base/OAuth2Base.swift index e4caa61a..1cfda1d0 100644 --- a/Sources/Base/OAuth2Base.swift +++ b/Sources/Base/OAuth2Base.swift @@ -2,8 +2,8 @@ // OAuth2Base.swift // OAuth2 // -// Created by Pascal Pfiffner on 6/2/15. -// Copyright 2015 Pascal Pfiffner +// Created by Pascal Pfiffner on 6/4/14. +// Copyright 2014 Pascal Pfiffner // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,280 +21,296 @@ import Foundation -/// Typealias to ease working with JSON dictionaries. -public typealias OAuth2JSON = [String: AnyObject] - -/// Typealias to work with dictionaries full of strings. -public typealias OAuth2StringDict = [String: String] - -/// Typealias to work with headers -public typealias OAuth2Headers = [String: String] - - /** -Abstract base class for OAuth2 authorization as well as client registration classes. +Class extending on OAuth2Requestable, exposing configuration and maintaining context, serving as base class for `OAuth2`. */ -public class OAuth2Base { +open class OAuth2Base: OAuth2Securable { - /// Server-side settings, as set upon initialization. - final let settings: OAuth2JSON + /// The grant type represented by the class, e.g. "authorization_code" for code grants. + open class var grantType: String { + return "__undefined" + } - /// Set to `true` to log all the things. `false` by default. Use `"verbose": bool` in settings or assign `logger` yourself. - public var verbose = false { - didSet { - logger = verbose ? OAuth2DebugLogger() : nil - } + /// The response type expected from an authorize call, e.g. "code" for code grants. + open class var responseType: String? { + return nil } - /// The logger being used. Auto-assigned to a debug logger if you set `verbose` to true or false. - public var logger: OAuth2Logger? + /// Settings related to the client-server relationship. + open let clientConfig: OAuth2ClientConfig - /// If set to `true` (the default) will use system keychain to store tokens. Use `"keychain": bool` in settings. - public var useKeychain = true { - didSet { - if useKeychain { - updateFromKeychain() - } - } + /// Client-side authorization options. + open var authConfig = OAuth2AuthConfig() + + /// The client id. + public final var clientId: String? { + get { return clientConfig.clientId } + set { clientConfig.clientId = newValue } } - /// The keychain account to use to store tokens. Defaults to "currentTokens". - public var keychainAccountForTokens = "currentTokens" { - didSet { - assert(!keychainAccountForTokens.isEmpty) - } + /// The client secret, usually only needed for code grant. + public final var clientSecret: String? { + get { return clientConfig.clientSecret } + set { clientConfig.clientSecret = newValue } } - /// The keychain account name to use to store client credentials. Defaults to "clientCredentials". - public var keychainAccountForClientCredentials = "clientCredentials" { - didSet { - assert(!keychainAccountForClientCredentials.isEmpty) - } + /// The name of the client, as used during dynamic client registration. Use "client_name" during initalization to set. + open var clientName: String? { + get { return clientConfig.clientName } + } + + /// The URL to authorize against. + public final var authURL: URL { + get { return clientConfig.authorizeURL } + } + + /// The URL string where we can exchange a code for a token; if nil `authURL` will be used. + public final var tokenURL: URL? { + get { return clientConfig.tokenURL } } - /// Defaults to `kSecAttrAccessibleWhenUnlocked` - public internal(set) var keychainAccessMode = kSecAttrAccessibleWhenUnlocked + /// The scope currently in use. + public final var scope: String? { + get { return clientConfig.scope } + set { clientConfig.scope = newValue } + } + /// The redirect URL string to use. + public final var redirect: String? { + get { return clientConfig.redirect } + set { clientConfig.redirect = newValue } + } + + /// Context for the current auth dance. + open var context = OAuth2ContextStore() + + /// The receiver's access token. + open var accessToken: String? { + get { return clientConfig.accessToken } + set { clientConfig.accessToken = newValue } + } + + /// The receiver's id token. + open var idToken: String? { + get { return clientConfig.idToken } + set { clientConfig.idToken = newValue } + } + + /// The access token's expiry date. + open var accessTokenExpiry: Date? { + get { return clientConfig.accessTokenExpiry } + set { clientConfig.accessTokenExpiry = newValue } + } + + /// The receiver's long-time refresh token. + open var refreshToken: String? { + get { return clientConfig.refreshToken } + set { clientConfig.refreshToken = newValue } + } + + /// Custom or overridden HTML headers to be used during authorization. + public var authHeaders: OAuth2Headers? { + get { return clientConfig.authHeaders } + set { clientConfig.authHeaders = newValue } + } + + /// Custom authorization parameters. + public var authParameters: OAuth2StringDict? { + get { return clientConfig.authParameters } + set { clientConfig.authParameters = newValue } + } + + + /// This closure is internally used with `authorize(params:callback:)` and only exposed for subclassing reason, do not mess with it! + public final var didAuthorizeOrFail: ((_ parameters: OAuth2JSON?, _ error: OAuth2Error?) -> Void)? + + /// Returns true if the receiver is currently authorizing. + public final var isAuthorizing: Bool { + return nil != didAuthorizeOrFail + } + + /// Closure called on successful authorization on the main thread. + @available(*, deprecated: 3.0, message: "Use the `authorize(params:callback:)` method and variants") + public final var onAuthorize: ((_ parameters: OAuth2JSON) -> Void)? + + /// When authorization fails (if error is not nil) or is cancelled, this block is executed on the main thread. + @available(*, deprecated: 3.0, message: "Use the `authorize(params:callback:)` method and variants") + public final var onFailure: ((OAuth2Error?) -> Void)? /** - Base initializer. + Closure called after the regular authorization callback, on the main thread. You can use this callback when you're performing + authorization manually and/or for cleanup operations. + + - parameter authParameters: All authorization parameters; non-nil (but possibly empty) on success, nil on error + - parameter error: OAuth2Error giving the failure reason; if nil and `authParameters` is also nil, the process was aborted. + */ + public final var afterAuthorizeOrFail: ((_ authParameters: OAuth2JSON?, _ error: OAuth2Error?) -> Void)? - Looks at the `keychain`, `keychain_access_mode` and `verbose` keys in the _settings_ dict. Everything else is handled by subclasses. + /** + For internal use, don't mess with it, it's public only for subclassing and compilation reasons. Executed right before + `afterAuthorizeOrFail`. */ - public init(settings: OAuth2JSON) { - self.settings = settings + public final var internalAfterAuthorizeOrFail: ((_ wasFailure: Bool, _ error: OAuth2Error?) -> Void)? + + + /** + Designated initializer. + + The following settings keys are currently supported: + + - client_id (string) + - 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 + - redirect_uris (list of URL-strings) + - scope (string) + + - client_name (string) + - registration_uri (URL-string) + - logo_uri (URL-string) + + - keychain (bool, true by default, applies to using the system keychain) + - keychain_access_mode (string, value for keychain kSecAttrAccessible attribute, kSecAttrAccessibleWhenUnlocked by default) + - keychain_access_group (string, value for keychain kSecAttrAccessGroup attribute, nil by default) + - verbose (bool, false by default, applies to client logging) + - secret_in_body (bool, false by default, forces the flow to use the request body for the client secret) + - token_assume_unexpired (bool, true by default, whether to use access tokens that do not come with an "expires_in" parameter) + */ + override public init(settings: OAuth2JSON) { + clientConfig = OAuth2ClientConfig(settings: settings) - // client settings - if let keychain = settings["keychain"] as? Bool { - useKeychain = keychain - } - if let accessMode = settings["keychain_access_mode"] as? String { - keychainAccessMode = accessMode + // auth configuration options + if let inBody = settings["secret_in_body"] as? Bool { + authConfig.secretInBody = inBody } - if let verb = settings["verbose"] as? Bool { - verbose = verb - if verbose { - logger = OAuth2DebugLogger() - } + if let ttl = settings["title"] as? String { + authConfig.ui.title = ttl } - - // init from keychain - if useKeychain { - updateFromKeychain() - } - logger?.debug("OAuth2", msg: "Initialization finished") + super.init(settings: settings) } // MARK: - Keychain Integration - /** The service key under which to store keychain items. Returns "http://localhost", subclasses override to return the authorize URL. */ - public func keychainServiceName() -> String { - return "http://localhost" + /** Overrides base implementation to return the authorize URL. */ + override open func keychainServiceName() -> String { + return authURL.description } - /** Queries the keychain for tokens stored for the receiver's authorize URL, and updates the token properties accordingly. */ - private func updateFromKeychain() { - logger?.debug("OAuth2", msg: "Looking for items in keychain") - - do { - var creds = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForClientCredentials) - let creds_data = try creds.fetchedFromKeychain() - updateFromKeychainItems(creds_data) - } - catch { - logger?.warn("OAuth2", msg: "Failed to load client credentials from keychain: \(error)") - } - - do { - var toks = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForTokens) - let toks_data = try toks.fetchedFromKeychain() - updateFromKeychainItems(toks_data) - } - catch { - logger?.warn("OAuth2", msg: "Failed to load tokens from keychain: \(error)") + override func updateFromKeychainItems(_ items: [String: Any]) { + for message in clientConfig.updateFromStorableItems(items) { + logger?.debug("OAuth2", msg: message) } + authConfig.secretInBody = (clientConfig.endpointAuthMethod == OAuth2EndpointAuthMethod.clientSecretPost) } - /** Updates instance properties according to the items found in the given dictionary, which was found in the keychain. */ - func updateFromKeychainItems(items: [String: NSCoding]) { + override open func storableCredentialItems() -> [String: Any]? { + return clientConfig.storableCredentialItems() } - /** - Items that should be stored when storing client credentials. + override open func storableTokenItems() -> [String: Any]? { + return clientConfig.storableTokenItems() + } - - returns: A dictionary with `String` keys and `NSCoding` adopting items - */ - public func storableCredentialItems() -> [String: NSCoding]? { - return nil + override open func forgetClient() { + super.forgetClient() + clientConfig.forgetCredentials() } - /** Stores our client credentials in the keychain. */ - internal func storeClientToKeychain() { - if let items = storableCredentialItems() { - logger?.debug("OAuth2", msg: "Storing client credentials to keychain") - let keychain = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForClientCredentials, data: items) - do { - try keychain.saveInKeychain() - } - catch { - logger?.warn("OAuth2", msg: "Failed to store client credentials to keychain: \(error)") - } - } + override open func forgetTokens() { + super.forgetTokens() + clientConfig.forgetTokens() } - /** - Items that should be stored when tokens are stored to the keychain. - - returns: A dictionary with `String` keys and `NSCoding` adopting items - */ - public func storableTokenItems() -> [String: NSCoding]? { - return nil - } + // MARK: - Request Signing - /** Stores our current token(s) in the keychain. */ - internal func storeTokensToKeychain() { - if let items = storableTokenItems() { - logger?.debug("OAuth2", msg: "Storing tokens to keychain") - let keychain = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForTokens, data: items) - do { - try keychain.saveInKeychain() - } - catch { - logger?.warn("OAuth2", msg: "Failed to store tokens to keychain: \(error)") - } - } - } + /** + Return an OAuth2Request, a NSMutableURLRequest subclass, that has already been signed and can be used against your OAuth2 endpoint. - /** Unsets the client credentials and deletes them from the keychain. */ - public func forgetClient() { - logger?.debug("OAuth2", msg: "Forgetting client credentials and removing them from keychain") - let keychain = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForClientCredentials) - do { - try keychain.removeFromKeychain() - } - catch { - logger?.warn("OAuth2", msg: "Failed to delete credentials from keychain: \(error)") - } - } + This method by default ignores locally cached data and specifies a timeout interval of 20 seconds. This should be ideal for small JSON + data requests, but you probably don't want to disable cache for binary data like avatars. - /** Unsets the tokens and deletes them from the keychain. */ - public func forgetTokens() { - logger?.debug("OAuth2", msg: "Forgetting tokens and removing them from keychain") - - let keychain = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForTokens) - do { - try keychain.removeFromKeychain() - } - catch { - logger?.warn("OAuth2", msg: "Failed to delete tokens from keychain: \(error)") - } + - parameter forURL: The URL to create a request for + - parameter cachePolicy: The cache policy to use, defaults to `NSURLRequestCachePolicy.ReloadIgnoringLocalCacheData` + - returns: OAuth2Request for the given URL + */ + open func request(forURL url: URL, cachePolicy: NSURLRequest.CachePolicy = .reloadIgnoringLocalCacheData) -> URLRequest { + var req = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: 20) + req.sign(with: self) + return req } - // MARK: - Requests + // MARK: - Callbacks - /// The instance's current session, creating one by the book if necessary. Defaults to using an ephemeral session, you can use - /// `sessionConfiguration` and/or `sessionDelegate` to affect how the session is configured. - public var session: NSURLSession { - if nil == _session { - let config = sessionConfiguration ?? NSURLSessionConfiguration.ephemeralSessionConfiguration() - _session = NSURLSession(configuration: config, delegate: sessionDelegate, delegateQueue: nil) - } - return _session! + /** + Subclasses override this method to extract information from the supplied redirect URL. + + - parameter redirect: The redirect URL returned by the server that you want to handle + */ + open func handleRedirectURL(_ redirect: URL) throws { + throw OAuth2Error.generic("Abstract class use") } - /// The backing store for `session`. - private var _session: NSURLSession? + /** + Internally used on success, calls the callbacks on the main thread. - /// The configuration to use when creating `session`. Uses an `+ephemeralSessionConfiguration()` if nil. - public var sessionConfiguration: NSURLSessionConfiguration? { - didSet { - _session = nil - } - } + This method is only made public in case you want to create a subclass and call `didAuthorize(parameters:)` at an override point. If you + call this method yourself on your OAuth2 instance you might screw things up badly. - /// URL session delegate that should be used for the `NSURLSession` the instance uses for requests. - public var sessionDelegate: NSURLSessionDelegate? { - didSet { - _session = nil + - parameter withParameters: The parameters received during authorization + */ + public final func didAuthorize(withParameters parameters: OAuth2JSON) { + if useKeychain { + storeTokensToKeychain() + } + callOnMainThread() { + self.onAuthorize?(parameters) + self.didAuthorizeOrFail?(parameters, nil) + self.didAuthorizeOrFail = nil + self.internalAfterAuthorizeOrFail?(false, nil) + self.afterAuthorizeOrFail?(parameters, nil) } } /** - Perform the supplied request and call the callback with the response JSON dict or an error. This method is intended for authorization - calls, not for data calls outside of the OAuth2 dance. + Internally used on error, calls the callbacks on the main thread with the appropriate error message. - This implementation uses the shared `NSURLSession` and executes a data task. If the server responds with an error, this will be - converted into an error according to information supplied in the response JSON (if availale). + This method is only made public in case you want to create a subclass and need to call `didFail(error:)` at an override point. If you + call this method yourself on your OAuth2 instance you might screw things up royally. - - parameter request: The request to execute - - parameter callback: The callback to call when the request completes/fails; data and error are mutually exclusive + - parameter error: The error that led to authorization failure; will use `.requestCancelled` on the callbacks if nil is passed */ - public func performRequest(request: NSURLRequest, callback: ((data: NSData?, status: Int?, error: ErrorType?) -> Void)) { - self.logger?.trace("OAuth2", msg: "REQUEST\n\(request.debugDescription)\n---") - let task = session.dataTaskWithRequest(request) { sessData, sessResponse, error in - self.abortableTask = nil - self.logger?.trace("OAuth2", msg: "RESPONSE\n\(sessResponse?.debugDescription ?? "no response")\n\n\(NSString(data: sessData ?? NSData(), encoding: NSUTF8StringEncoding) ?? "no data")\n---") - if let error = error { - if NSURLErrorDomain == error.domain && -999 == error.code { // request was cancelled - callback(data: nil, status: nil, error: OAuth2Error.RequestCancelled) - } - else { - callback(data: nil, status: nil, error: error) - } - } - else if let data = sessData, let http = sessResponse as? NSHTTPURLResponse { - callback(data: data, status: http.statusCode, error: nil) - } - else { - let error = OAuth2Error.Generic("Unknown response \(sessResponse) with data “\(NSString(data: sessData!, encoding: NSUTF8StringEncoding))”") - callback(data: nil, status: nil, error: error) - } + public final func didFail(with error: OAuth2Error?) { + var finalError = error + if let error = finalError { + logger?.debug("OAuth2", msg: "\(error)") + } + else { + finalError = OAuth2Error.requestCancelled + } + callOnMainThread() { + self.onFailure?(finalError) + self.didAuthorizeOrFail?(nil, finalError) + self.didAuthorizeOrFail = nil + self.internalAfterAuthorizeOrFail?(true, finalError) + self.afterAuthorizeOrFail?(nil, finalError) } - abortableTask = task - task.resume() } - /// Currently running abortable session task. - private var abortableTask: NSURLSessionTask? - /** - Can be called to immediately abort the currently running authorization request, if it was started by `performRequest()`. - - - returns: A bool indicating whether a task was aborted or not + Allows to abort authorization currently in progress. */ - func abortTask() -> Bool { - guard let task = abortableTask else { - return false + open func abortAuthorization() { + if !abortTask() { + logger?.debug("OAuth2", msg: "Aborting authorization") + didFail(with: nil) } - logger?.debug("OAuth2", msg: "Aborting request") - task.cancel() - return true } - // MARK: - Response Verification + // MARK: - Response Parsing /** Handles access token error response. @@ -303,11 +319,11 @@ public class OAuth2Base { - parameter fallback: The message string to use in case no error description is found in the parameters - returns: An OAuth2Error */ - public func assureNoErrorInResponse(params: OAuth2JSON, fallback: String? = nil) throws { + open func assureNoErrorInResponse(_ params: OAuth2JSON, fallback: String? = nil) throws { // "error_description" is optional, we prefer it if it's present if let err_msg = params["error_description"] as? String { - throw OAuth2Error.ResponseError(err_msg) + throw OAuth2Error.responseError(err_msg) } // the "error" response is required for error responses, so it should be present @@ -316,57 +332,177 @@ public class OAuth2Base { } } + /** + Parse response data returned while exchanging the code for a token. + + This method expects token data to be JSON, decodes JSON and fills the receiver's properties accordingly. If the response contains an + "error" key, will parse the error and throw it. - // MARK: - Utilities + - parameter data: NSData returned from the call + - returns: An OAuth2JSON instance with token data; may contain additional information + */ + open func parseAccessTokenResponse(data: Data) throws -> OAuth2JSON { + let dict = try parseJSON(data) + return try parseAccessTokenResponse(params: dict) + } /** - Parse string-only JSON from NSData. + Parse response data returned while exchanging the code for a token. - - parameter data: NSData returned from the call, assumed to be JSON with string-values only. - - returns: An OAuth2JSON instance + This method extracts token data and fills the receiver's properties accordingly. If the response contains an "error" key, will parse the + error and throw it. The method is final to ensure correct order of error parsing and not parsing the response if an error happens. This + is not possible in overrides. Instead, override the various `assureXy(dict:)` methods, especially `assureAccessTokenParamsAreValid()`. + + - parameter params: Dictionary data parsed from the response + - returns: An OAuth2JSON instance with token data; may contain additional information */ - func parseJSON(data: NSData) throws -> OAuth2JSON { - if let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? OAuth2JSON { - return json - } - if let str = NSString(data: data, encoding: NSUTF8StringEncoding) { - logger?.warn("OAuth2", msg: "Unparsable JSON was: \(str)") - } - throw OAuth2Error.JSONParserError + public final func parseAccessTokenResponse(params: OAuth2JSON) throws -> OAuth2JSON { + try assureNoErrorInResponse(params) + try assureCorrectBearerType(params) + try assureAccessTokenParamsAreValid(params) + + clientConfig.updateFromResponse(normalizeAccessTokenResponseKeys(params)) + return params + } + + /** + This method does nothing, but allows subclasses to fix parameter names before passing the access token response to `OAuth2ClientConfig`s + `updateFromResponse()`. + + - parameter dict: The dictionary that was returned from an access token response + - returns: The dictionary with fixed key names + */ + open func normalizeAccessTokenResponseKeys(_ dict: OAuth2JSON) -> OAuth2JSON { + return dict } /** - Parse a query string into a dictionary of String: String pairs. + Parse response data returned while using a refresh token. - If you're retrieving a query or fragment from NSURLComponents, use the `percentEncoded##` variant as the others - automatically perform percent decoding, potentially messing with your query string. + This method extracts token data, expected to be JSON, and fills the receiver's properties accordingly. If the response contains an + "error" key, will parse the error and throw it. - - parameter query: The query string you want to have parsed - - returns: A dictionary full of strings with the key-value pairs found in the query + - parameter data: NSData returned from the call + - returns: An OAuth2JSON instance with token data; may contain additional information */ - public final class func paramsFromQuery(query: String) -> OAuth2StringDict { - let parts = query.characters.split() { $0 == "&" }.map() { String($0) } - var params = OAuth2StringDict(minimumCapacity: parts.count) - for part in parts { - let subparts = part.characters.split() { $0 == "=" }.map() { String($0) } - if 2 == subparts.count { - params[subparts[0]] = subparts[1].wwwFormURLDecodedString + open func parseRefreshTokenResponseData(_ data: Data) throws -> OAuth2JSON { + let dict = try parseJSON(data) + return try parseRefreshTokenResponse(dict) + } + + /** + Parse response data returned while using a refresh token. + + This method extracts token data and fills the receiver's properties accordingly. If the response contains an "error" key, will parse the + error and throw it. The method is final to ensure correct order of error parsing and not parsing the response if an error happens. This + is not possible in overrides. Instead, override the various `assureXy(dict:)` methods, especially `assureRefreshTokenParamsAreValid()`. + + - parameter json: Dictionary data parsed from the response + - returns: An OAuth2JSON instance with token data; may contain additional information + */ + final func parseRefreshTokenResponse(_ dict: OAuth2JSON) throws -> OAuth2JSON { + try assureNoErrorInResponse(dict) + try assureCorrectBearerType(dict) + try assureRefreshTokenParamsAreValid(dict) + + clientConfig.updateFromResponse(dict) + return dict + } + + /** + This method does nothing, but allows subclasses to fix parameter names before passing the refresh token response to + `OAuth2ClientConfig`s `updateFromResponse()`. + + - parameter dict: The dictionary that was returned from a refresh token response + - returns: The dictionary with fixed key names + */ + open func normalizeRefreshTokenResponseKeys(_ dict: OAuth2JSON) -> OAuth2JSON { + return dict + } + + /** + This method checks `state`, throws `OAuth2Error.missingState` or `OAuth2Error.invalidState`. Resets state if it matches. + */ + public final func assureMatchesState(_ params: OAuth2JSON) throws { + guard let state = params["state"] as? String, !state.isEmpty else { + throw OAuth2Error.missingState + } + logger?.trace("OAuth2", msg: "Checking state, got “\(state)”, expecting “\(context.state)”") + if !context.matchesState(state) { + throw OAuth2Error.invalidState + } + context.resetState() + } + + /** + Throws unless "token_type" is "bearer" (case-insensitive). + */ + open func assureCorrectBearerType(_ params: OAuth2JSON) throws { + if let tokType = params["token_type"] as? String { + if "bearer" == tokType.lowercased() { + return } + throw OAuth2Error.unsupportedTokenType("Only “bearer” token is supported, but received “\(tokType)”") } - return params + throw OAuth2Error.noTokenType + } + + /** + Called when parsing the access token response. Does nothing by default, implicit grant flows check state. + */ + open func assureAccessTokenParamsAreValid(_ params: OAuth2JSON) throws { + } + + /** + Called when parsing the refresh token response. Does nothing by default. + */ + open func assureRefreshTokenParamsAreValid(_ params: OAuth2JSON) throws { } } /** -Helper function to ensure that the callback is executed on the main thread. +Class, internally used, to store current authorization context, such as state and redirect-url. */ -func callOnMainThread(callback: (Void -> Void)) { - if NSThread.isMainThread() { - callback() +open class OAuth2ContextStore { + + /// Currently used redirect_url. + open var redirectURL: String? + + /// The current state. + internal var _state = "" + + /** + The state sent to the server when requesting a token. + + We internally generate a UUID and use the first 8 chars if `_state` is empty. + */ + open var state: String { + if _state.isEmpty { + _state = UUID().uuidString + _state = _state[_state.startIndex..<_state.index(_state.startIndex, offsetBy: 8)] // only use the first 8 chars, should be enough + } + return _state } - else { - dispatch_sync(dispatch_get_main_queue(), callback) + + /** + Checks that given state matches the internal state. + + - parameter state: The state to check (may be nil) + - returns: true if state matches, false otherwise or if given state is nil. + */ + func matchesState(_ state: String?) -> Bool { + if let st = state { + return st == _state + } + return false + } + + /** + Resets current state so it gets regenerated next time it's needed. + */ + func resetState() { + _state = "" } } diff --git a/Sources/Base/OAuth2ClientConfig.swift b/Sources/Base/OAuth2ClientConfig.swift index 816ed0cb..f175443a 100644 --- a/Sources/Base/OAuth2ClientConfig.swift +++ b/Sources/Base/OAuth2ClientConfig.swift @@ -12,7 +12,7 @@ import Foundation /** Client configuration object that holds on to client-server specific configurations such as client id, -secret and server URLs. */ -public class OAuth2ClientConfig { +open class OAuth2ClientConfig { /// The client id. public final var clientId: String? @@ -24,47 +24,50 @@ public class OAuth2ClientConfig { public final var clientName: String? /// The URL to authorize against. - public final let authorizeURL: NSURL + public final let authorizeURL: URL /// The URL where we can exchange a code for a token. - public final var tokenURL: NSURL? + public final var tokenURL: URL? /// Where a logo/icon for the app can be found. - public final var logoURL: NSURL? + public final var logoURL: URL? /// The scope currently in use. - public var scope: String? + open var scope: String? /// The redirect URL string currently in use. - public var redirect: String? + open var redirect: String? /// All redirect URLs passed to the initializer. - public var redirectURLs: [String]? + open var redirectURLs: [String]? /// The receiver's access token. - public var accessToken: String? + open var accessToken: String? /// The receiver's id token. Used by Google + and AWS Cognito - public var idToken: String? + open var idToken: String? /// The access token's expiry date. - public var accessTokenExpiry: NSDate? + open var accessTokenExpiry: Date? /// If set to true (the default), uses a keychain-supplied access token even if no "expires_in" parameter was supplied. - public var accessTokenAssumeUnexpired = true + open var accessTokenAssumeUnexpired = true /// The receiver's long-time refresh token. - public var refreshToken: String? + open var refreshToken: String? /// The URL to register a client against. - public final var registrationURL: NSURL? - - /// Contains parameters headers. - public var authHeaders: OAuth2Headers? + public final var registrationURL: URL? - /// How the client communicates the client secret with the server. Defaults to ".None" if there is no secret, ".ClientSecretPost" if - /// "secret_in_body" is `true` and ".ClientSecretBasic" otherwise. Interacts with the `authConfig.secretInBody` client setting. - public final var endpointAuthMethod = OAuth2EndpointAuthMethod.None + /// How the client communicates the client secret with the server. Defaults to ".None" if there is no secret, ".clientSecretPost" if + /// "secret_in_body" is `true` and ".clientSecretBasic" otherwise. Interacts with the `authConfig.secretInBody` client setting. + public final var endpointAuthMethod = OAuth2EndpointAuthMethod.none + + /// Contains special authorization request headers, can be used to override defaults. + open var authHeaders: OAuth2Headers? + + /// Custom request parameters to be added during authorization. + open var authParameters: OAuth2StringDict? /** @@ -76,43 +79,46 @@ public class OAuth2ClientConfig { clientName = settings["client_name"] as? String // authorize URL - var aURL: NSURL? + var aURL: URL? if let auth = settings["authorize_uri"] as? String { - aURL = NSURL(string: auth) + aURL = URL(string: auth) } - authorizeURL = aURL ?? NSURL(string: "http://localhost")! + authorizeURL = aURL ?? URL(string: "https://localhost/p2.OAuth2.defaultAuthorizeURI")! // token, registration and logo URLs if let token = settings["token_uri"] as? String { - tokenURL = NSURL(string: token) + tokenURL = URL(string: token) } if let registration = settings["registration_uri"] as? String { - registrationURL = NSURL(string: registration) + registrationURL = URL(string: registration) } if let logo = settings["logo_uri"] as? String { - logoURL = NSURL(string: logo) + logoURL = URL(string: logo) } - // client authentication options + // client authorization options scope = settings["scope"] as? String if let redirs = settings["redirect_uris"] as? [String] { redirectURLs = redirs redirect = redirs.first } - if let inBody = settings["secret_in_body"] as? Bool where inBody { - endpointAuthMethod = .ClientSecretPost + if let inBody = settings["secret_in_body"] as? Bool, inBody { + endpointAuthMethod = .clientSecretPost } else if nil != clientSecret { - endpointAuthMethod = .ClientSecretBasic + endpointAuthMethod = .clientSecretBasic + } + if let headers = settings["headers"] as? OAuth2Headers { + authHeaders = headers + } + if let params = settings["parameters"] as? OAuth2StringDict { + authParameters = params } // access token options if let assume = settings["token_assume_unexpired"] as? Bool { accessTokenAssumeUnexpired = assume } - if let headers = settings["headers"] as? OAuth2Headers { - authHeaders = headers - } } @@ -123,7 +129,7 @@ public class OAuth2ClientConfig { - parameter json: JSON data returned from a request */ - func updateFromResponse(json: OAuth2JSON) { + func updateFromResponse(_ json: OAuth2JSON) { if let access = json["access_token"] as? String { accessToken = access } @@ -131,11 +137,14 @@ public class OAuth2ClientConfig { idToken = idtoken } accessTokenExpiry = nil - if let expires = json["expires_in"] as? NSTimeInterval { - accessTokenExpiry = NSDate(timeIntervalSinceNow: expires) + if let expires = json["expires_in"] as? TimeInterval { + accessTokenExpiry = Date(timeIntervalSinceNow: expires) + } + else if let expires = json["expires_in"] as? Int { + accessTokenExpiry = Date(timeIntervalSinceNow: Double(expires)) } else if let expires = json["expires_in"] as? String { // when parsing implicit grant from URL fragment - accessTokenExpiry = NSDate(timeIntervalSinceNow: Double(expires) ?? 0.0) + accessTokenExpiry = Date(timeIntervalSinceNow: Double(expires) ?? 0.0) } if let refresh = json["refresh_token"] as? String { refreshToken = refresh @@ -147,10 +156,10 @@ public class OAuth2ClientConfig { - returns: A storable dictionary with credentials */ - func storableCredentialItems() -> [String: NSCoding]? { - guard let clientId = clientId where !clientId.isEmpty else { return nil } + func storableCredentialItems() -> [String: Any]? { + guard let clientId = clientId, !clientId.isEmpty else { return nil } - var items: [String: NSCoding] = ["id": clientId] + var items: [String: Any] = ["id": clientId] if let secret = clientSecret { items["secret"] = secret } @@ -163,20 +172,19 @@ public class OAuth2ClientConfig { - returns: A storable dictionary with token data */ - func storableTokenItems() -> [String: NSCoding]? { - guard let access = accessToken where !access.isEmpty else { return nil } + func storableTokenItems() -> [String: Any]? { + guard let access = accessToken, !access.isEmpty else { return nil } - var items: [String: NSCoding] = ["accessToken": access] - if let date = accessTokenExpiry where date == date.laterDate(NSDate()) { + var items: [String: Any] = ["accessToken": access] + if let date = accessTokenExpiry, date == (date as NSDate).laterDate(Date()) { items["accessTokenDate"] = date } - if let refresh = refreshToken where !refresh.isEmpty { + if let refresh = refreshToken, !refresh.isEmpty { items["refreshToken"] = refresh } - if let idtoken = idToken where !idtoken.isEmpty { + if let idtoken = idToken, !idtoken.isEmpty { items["idToken"] = idtoken } - return items } @@ -186,7 +194,7 @@ public class OAuth2ClientConfig { - parameter items: The dictionary representation of the data to store to keychain - returns: An array of strings containing log messages */ - func updateFromStorableItems(items: [String: NSCoding]) -> [String] { + func updateFromStorableItems(_ items: [String: Any]) -> [String] { var messages = [String]() if let id = items["id"] as? String { clientId = id @@ -199,9 +207,9 @@ public class OAuth2ClientConfig { if let methodName = items["endpointAuthMethod"] as? String, let method = OAuth2EndpointAuthMethod(rawValue: methodName) { endpointAuthMethod = method } - if let token = items["accessToken"] as? String where !token.isEmpty { - if let date = items["accessTokenDate"] as? NSDate { - if date == date.laterDate(NSDate()) { + if let token = items["accessToken"] as? String, !token.isEmpty { + if let date = items["accessTokenDate"] as? Date { + if date == (date as NSDate).laterDate(Date()) { messages.append("Found access token, valid until \(date)") accessTokenExpiry = date accessToken = token @@ -218,11 +226,11 @@ public class OAuth2ClientConfig { messages.append("Found access token but no expiration date, discarding (set `accessTokenAssumeUnexpired` to true to still use it)") } } - if let token = items["refreshToken"] as? String where !token.isEmpty { + if let token = items["refreshToken"] as? String, !token.isEmpty { messages.append("Found refresh token") refreshToken = token } - if let idtoken = items["idToken"] as? String where !idtoken.isEmpty { + if let idtoken = items["idToken"] as? String, !idtoken.isEmpty { messages.append("Found id token") idToken = idtoken } @@ -230,13 +238,13 @@ public class OAuth2ClientConfig { } /** Forgets the configuration's client id and secret. */ - public func forgetCredentials() { + open func forgetCredentials() { clientId = nil clientSecret = nil } /** Forgets the configuration's current tokens. */ - public func forgetTokens() { + open func forgetTokens() { accessToken = nil accessTokenExpiry = nil refreshToken = nil diff --git a/Sources/Base/OAuth2DebugURLSessionDelegate.swift b/Sources/Base/OAuth2DebugURLSessionDelegate.swift new file mode 100644 index 00000000..34bb657c --- /dev/null +++ b/Sources/Base/OAuth2DebugURLSessionDelegate.swift @@ -0,0 +1,53 @@ +// +// OAuth2DebugURLSessionDelegate.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 6/24/14. +// Copyright 2014 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +/** +An URLSession delegate that allows you to use self-signed SSL certificates. + +Doing so is a REALLY BAD IDEA, even in development environments where you can use real, free certificates that are valid a few months. +Still, sometimes you'll have to do this so this class is provided, but DO NOT SUBMIT your app using self-signed SSL certs to the App +Store. You have been warned! +*/ +open class OAuth2DebugURLSessionDelegate: NSObject, URLSessionDelegate { + + /// The host to allow a self-signed SSL certificate for. + let host: String + + public init(host: String) { + self.host = host + } + + @nonobjc + open func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, + completionHandler: (Foundation.URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if challenge.protectionSpace.host == host, let trust = challenge.protectionSpace.serverTrust { + let credential = URLCredential(trust: trust) + completionHandler(.useCredential, credential) + return + } + } + completionHandler(.cancelAuthenticationChallenge, nil) + } +} + diff --git a/Sources/Base/OAuth2Error.swift b/Sources/Base/OAuth2Error.swift index 6ae52f75..e133c5a7 100644 --- a/Sources/Base/OAuth2Error.swift +++ b/Sources/Base/OAuth2Error.swift @@ -26,115 +26,127 @@ All errors that might occur. The response errors return a description as defined in the spec: http://tools.ietf.org/html/rfc6749#section-4.1.2.1 */ -public enum OAuth2Error: ErrorType, CustomStringConvertible, Equatable { +public enum OAuth2Error: Error, CustomStringConvertible, Equatable { /// An error for which we don't have a specific one. - case Generic(String) + case generic(String) /// An error holding on to an NSError. - case NSError(Foundation.NSError) + case nsError(Foundation.NSError) /// Invalid URL components, failed to create a URL - case InvalidURLComponents(NSURLComponents) + case invalidURLComponents(URLComponents) // MARK: - Client errors /// There is no client id. - case NoClientId + case noClientId /// There is no client secret. - case NoClientSecret + case noClientSecret /// There is no redirect URL. - case NoRedirectURL + case noRedirectURL /// There is no username. - case NoUsername + case noUsername /// There is no password. - case NoPassword + case noPassword + + /// The client is already authorizing. + case alreadyAuthorizing /// There is no authorization context. - case NoAuthorizationContext + case noAuthorizationContext /// The authorization context is invalid. - case InvalidAuthorizationContext + case invalidAuthorizationContext /// The redirect URL is invalid; with explanation. - case InvalidRedirectURL(String) + case invalidRedirectURL(String) /// There is no refresh token. - case NoRefreshToken + case noRefreshToken /// There is no registration URL. - case NoRegistrationURL + case noRegistrationURL // MARK: - Request errors /// The request is not using SSL/TLS. - case NotUsingTLS + case notUsingTLS /// Unable to open the authorize URL. - case UnableToOpenAuthorizeURL + case unableToOpenAuthorizeURL /// The request is invalid. - case InvalidRequest + case invalidRequest /// The request was cancelled. - case RequestCancelled + case requestCancelled // MARK: - Response Errors /// There was no token type in the response. - case NoTokenType + case noTokenType /// The token type is not supported. - case UnsupportedTokenType(String) + case unsupportedTokenType(String) /// There was no data in the response. - case NoDataInResponse + case noDataInResponse /// Some prerequisite failed; with explanation. - case PrerequisiteFailed(String) + case prerequisiteFailed(String) + + /// The state parameter was missing in the response. + case missingState /// The state parameter was invalid. - case InvalidState + case invalidState /// The JSON response could not be parsed. - case JSONParserError + case jsonParserError /// Unable to UTF-8 encode. - case UTF8EncodeError + case utf8EncodeError /// Unable to decode to UTF-8. - case UTF8DecodeError + case utf8DecodeError // MARK: - OAuth2 errors - /// The client is unauthorized. - case UnauthorizedClient + /// The client is unauthorized (HTTP status 401). + case unauthorizedClient + + /// The request was forbidden (HTTP status 403). + case forbidden + + /// Username or password was wrong (HTTP status 403 on password grant). + case wrongUsernamePassword /// Access was denied. - case AccessDenied + case accessDenied /// Response type is not supported. - case UnsupportedResponseType + case unsupportedResponseType /// Scope was invalid. - case InvalidScope + case invalidScope /// A 500 was thrown. - case ServerError + case serverError /// The service is temporarily unavailable. - case TemporarilyUnavailable + case temporarilyUnavailable /// Other response error, as defined in its String. - case ResponseError(String) + case responseError(String) /** @@ -144,140 +156,168 @@ public enum OAuth2Error: ErrorType, CustomStringConvertible, Equatable { - parameter fallback: The error string to use in case the error code is not known - returns: An appropriate OAuth2Error */ - public static func fromResponseError(code: String, fallback: String? = nil) -> OAuth2Error { + public static func fromResponseError(_ code: String, fallback: String? = nil) -> OAuth2Error { switch code { case "invalid_request": - return .InvalidRequest + return .invalidRequest case "unauthorized_client": - return .UnauthorizedClient + return .unauthorizedClient case "access_denied": - return .AccessDenied + return .accessDenied case "unsupported_response_type": - return .UnsupportedResponseType + return .unsupportedResponseType case "invalid_scope": - return .InvalidScope + return .invalidScope case "server_error": - return .ServerError + return .serverError case "temporarily_unavailable": - return .TemporarilyUnavailable + return .temporarilyUnavailable default: - return .ResponseError(fallback ?? "Authorization error: \(code)") + return .responseError(fallback ?? "Authorization error: \(code)") } } /// Human understandable error string. public var description: String { switch self { - case .Generic(let message): + case .generic(let message): return message - case .NSError(let error): + case .nsError(let error): return error.localizedDescription - case .InvalidURLComponents(let components): + case .invalidURLComponents(let components): return "Failed to create URL from components: \(components)" - case NoClientId: + case .noClientId: return "Client id not set" - case NoClientSecret: + case .noClientSecret: return "Client secret not set" - case NoRedirectURL: + case .noRedirectURL: return "Redirect URL not set" - case NoUsername: + case .noUsername: return "No username" - case NoPassword: + case .noPassword: return "No password" - case NoAuthorizationContext: + case .alreadyAuthorizing: + return "The client is already authorizing, wait for it to finish or abort authorization before trying again" + case .noAuthorizationContext: return "No authorization context present" - case InvalidAuthorizationContext: + case .invalidAuthorizationContext: return "Invalid authorization context" - case InvalidRedirectURL(let url): + case .invalidRedirectURL(let url): return "Invalid redirect URL: \(url)" - case .NoRefreshToken: + case .noRefreshToken: return "I don't have a refresh token, not trying to refresh" - case .NoRegistrationURL: + case .noRegistrationURL: return "No registration URL defined" - case .NotUsingTLS: + case .notUsingTLS: return "You MUST use HTTPS/SSL/TLS" - case .UnableToOpenAuthorizeURL: + case .unableToOpenAuthorizeURL: return "Cannot open authorize URL" - case .InvalidRequest: + case .invalidRequest: return "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed." - case .RequestCancelled: + case .requestCancelled: return "The request has been cancelled" - case NoTokenType: + case .noTokenType: return "No token type received, will not use the token" - case UnsupportedTokenType(let message): + case .unsupportedTokenType(let message): return message - case NoDataInResponse: + case .noDataInResponse: return "No data in the response" - case PrerequisiteFailed(let message): + case .prerequisiteFailed(let message): return message - case InvalidState: - return "The state was either empty or did not check out" - case JSONParserError: + case .missingState: + return "The state parameter was missing in the response" + case .invalidState: + return "The state parameter did not check out" + case .jsonParserError: return "Error parsing JSON" - case UTF8EncodeError: + case .utf8EncodeError: return "Failed to UTF-8 encode the given string" - case UTF8DecodeError: + case .utf8DecodeError: return "Failed to decode given data as a UTF-8 string" - case .UnauthorizedClient: + case .unauthorizedClient: return "The client is not authorized to request an access token using this method." - case .AccessDenied: + case .forbidden: + return "Forbidden" + case .wrongUsernamePassword: + return "The username or password is incorrect" + case .accessDenied: return "The resource owner or authorization server denied the request." - case .UnsupportedResponseType: + case .unsupportedResponseType: return "The authorization server does not support obtaining an access token using this method." - case .InvalidScope: + case .invalidScope: return "The requested scope is invalid, unknown, or malformed." - case .ServerError: + case .serverError: return "The authorization server encountered an unexpected condition that prevented it from fulfilling the request." - case .TemporarilyUnavailable: + case .temporarilyUnavailable: return "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server." - case .ResponseError(let message): + case .responseError(let message): return message } } + + + // MARK: - Equatable + + public static func ==(lhs: OAuth2Error, rhs: OAuth2Error) -> Bool { + switch (lhs, rhs) { + case (.generic(let lhm), .generic(let rhm)): return lhm == rhm + case (.nsError(let lhe), .nsError(let rhe)): return lhe.isEqual(rhe) + case (.invalidURLComponents(let lhe), .invalidURLComponents(let rhe)): return (lhe == rhe) + + case (.noClientId, .noClientId): return true + case (.noClientSecret, .noClientSecret): return true + case (.noRedirectURL, .noRedirectURL): return true + case (.noUsername, .noUsername): return true + case (.noPassword, .noPassword): return true + case (.alreadyAuthorizing, .alreadyAuthorizing): return true + case (.noAuthorizationContext, .noAuthorizationContext): return true + case (.invalidAuthorizationContext, .invalidAuthorizationContext): return true + case (.invalidRedirectURL(let lhu), .invalidRedirectURL(let rhu)): return lhu == rhu + case (.noRefreshToken, .noRefreshToken): return true + + case (.notUsingTLS, .notUsingTLS): return true + case (.unableToOpenAuthorizeURL, .unableToOpenAuthorizeURL): return true + case (.invalidRequest, .invalidRequest): return true + case (.requestCancelled, .requestCancelled): return true + case (.noTokenType, .noTokenType): return true + case (.unsupportedTokenType(let lhm), .unsupportedTokenType(let rhm)): return lhm == rhm + case (.noDataInResponse, .noDataInResponse): return true + case (.prerequisiteFailed(let lhm), .prerequisiteFailed(let rhm)): return lhm == rhm + case (.missingState, .missingState): return true + case (.invalidState, .invalidState): return true + case (.jsonParserError, .jsonParserError): return true + case (.utf8EncodeError, .utf8EncodeError): return true + case (.utf8DecodeError, .utf8DecodeError): return true + + case (.unauthorizedClient, .unauthorizedClient): return true + case (.forbidden, .forbidden): return true + case (.wrongUsernamePassword, .wrongUsernamePassword): return true + case (.accessDenied, .accessDenied): return true + case (.unsupportedResponseType, .unsupportedResponseType): return true + case (.invalidScope, .invalidScope): return true + case (.serverError, .serverError): return true + case (.temporarilyUnavailable, .temporarilyUnavailable): return true + case (.responseError(let lhm), .responseError(let rhm)): return lhm == rhm + default: return false + } + } } -public func ==(lhs: OAuth2Error, rhs: OAuth2Error) -> Bool { - switch (lhs, rhs) { - case (.Generic(let lhm), .Generic(let rhm)): return lhm == rhm - case (.NSError(let lhe), .NSError(let rhe)): return lhe.isEqual(rhe) - case (.InvalidURLComponents(let lhe), .InvalidURLComponents(let rhe)): return lhe.isEqual(rhe) - - case (.NoClientId, .NoClientId): return true - case (.NoClientSecret, .NoClientSecret): return true - case (.NoRedirectURL, .NoRedirectURL): return true - case (.NoUsername, .NoUsername): return true - case (.NoPassword, .NoPassword): return true - case (.NoAuthorizationContext, .NoAuthorizationContext): return true - case (.InvalidAuthorizationContext, .InvalidAuthorizationContext): return true - case (.InvalidRedirectURL(let lhu), .InvalidRedirectURL(let rhu)): return lhu == rhu - case (.NoRefreshToken, .NoRefreshToken): return true - - case (.NotUsingTLS, .NotUsingTLS): return true - case (.UnableToOpenAuthorizeURL, .UnableToOpenAuthorizeURL): return true - case (.InvalidRequest, .InvalidRequest): return true - case (.RequestCancelled, .RequestCancelled): return true - case (.NoTokenType, .NoTokenType): return true - case (.UnsupportedTokenType(let lhm), .UnsupportedTokenType(let rhm)): return lhm == rhm - case (.NoDataInResponse, .NoDataInResponse): return true - case (.PrerequisiteFailed(let lhm), .PrerequisiteFailed(let rhm)): return lhm == rhm - case (.InvalidState, .InvalidState): return true - case (.JSONParserError, .JSONParserError): return true - case (.UTF8EncodeError, .UTF8EncodeError): return true - case (.UTF8DecodeError, .UTF8DecodeError): return true - - case (.UnauthorizedClient, .UnauthorizedClient): return true - case (.AccessDenied, .AccessDenied): return true - case (.UnsupportedResponseType, .UnsupportedResponseType): return true - case (.InvalidScope, .InvalidScope): return true - case (.ServerError, .ServerError): return true - case (.TemporarilyUnavailable, .TemporarilyUnavailable): return true - case (.ResponseError(let lhm), .ResponseError(let rhm)): return lhm == rhm - default: return false +public extension Error { + + /** + Convenience getter to easily retrieve an OAuth2Error from any Error. + */ + public var asOAuth2Error: OAuth2Error { + if let oaerror = self as? OAuth2Error { + return oaerror + } + return OAuth2Error.nsError(self as NSError) } } diff --git a/Sources/Base/OAuth2KeychainAccount.swift b/Sources/Base/OAuth2KeychainAccount.swift index b31948a4..2f7411ed 100644 --- a/Sources/Base/OAuth2KeychainAccount.swift +++ b/Sources/Base/OAuth2KeychainAccount.swift @@ -19,7 +19,7 @@ // import Foundation -#if !NO_KEYCHAIN_IMPORT // needs to be imported when using `swift build` or with CocoaPods, not when building via Xcode +#if !NO_KEYCHAIN_IMPORT // needs to be imported when using `swift build`, not when building via Xcode import SwiftKeychain #endif @@ -35,17 +35,21 @@ struct OAuth2KeychainAccount: KeychainGenericPasswordType { /// The account name to use. let accountName: String + /// The keychain access group. + var accessGroup: String? + /// Data that ends up in the keychain. - var data = [String: AnyObject]() + var data = [String: Any]() /// Keychain access mode. let accessMode: String - init(oauth2: OAuth2Base, account: String, data inData: [String: AnyObject] = [:]) { + init(oauth2: OAuth2Securable, account: String, data inData: [String: Any] = [:]) { serviceName = oauth2.keychainServiceName() accountName = account accessMode = String(oauth2.keychainAccessMode) + accessGroup = oauth2.keychainAccessGroup data = inData } } @@ -53,24 +57,21 @@ struct OAuth2KeychainAccount: KeychainGenericPasswordType { extension KeychainGenericPasswordType { - var dataToStore: [String: AnyObject] { + var dataToStore: [String: Any] { return data } /** Attempts to read data from the keychain, will ignore `errSecItemNotFound` but throw others. - - returns: A [String: NSCoding] dictionary of data fetched from the keychain + - returns: A [String: Any] dictionary of data fetched from the keychain */ - mutating func fetchedFromKeychain() throws -> [String: NSCoding] { + mutating func fetchedFromKeychain() throws -> [String: Any] { do { - try fetchFromKeychain() - if let creds_data = data as? [String: NSCoding] { - return creds_data - } - throw OAuth2Error.Generic("Keychain data for \(serviceName) > \(accountName) is in wrong format. Got: “\(data)”") + try _ = fetchFromKeychain() + return data } - catch let error as NSError where error.domain == "swift.keychain.error" && error.code == Int(errSecItemNotFound) { + catch let error where error._domain == "swift.keychain.error" && error._code == Int(errSecItemNotFound) { return [:] } } diff --git a/Sources/Base/OAuth2Logger.swift b/Sources/Base/OAuth2Logger.swift index ede61ce0..a315205a 100644 --- a/Sources/Base/OAuth2Logger.swift +++ b/Sources/Base/OAuth2Logger.swift @@ -25,26 +25,26 @@ Logging levels public enum OAuth2LogLevel: Int, CustomStringConvertible { /// If you want the logger to log everything. - case Trace = 0 + case trace = 0 /// Only log debug messages. - case Debug + case debug /// Only warning messages. - case Warn + case warn /// Don't log anything. - case Off + case off public var description: String { switch self { - case .Trace: + case .trace: return "Trace" - case .Debug: + case .debug: return "Debug" - case .Warn: + case .warn: return "Warn!" - case .Off: + case .off: return "-/-" } } @@ -69,13 +69,13 @@ public protocol OAuth2Logger { var level: OAuth2LogLevel { get } /** Log a message at the trace level. */ - func trace(module: String?, filename: String?, line: Int?, function: String?, @autoclosure msg: () -> String) + func trace(_ module: String?, filename: String?, line: Int?, function: String?, msg: @autoclosure() -> String) /** Standard debug logging. */ - func debug(module: String?, filename: String?, line: Int?, function: String?, @autoclosure msg: () -> String) + func debug(_ module: String?, filename: String?, line: Int?, function: String?, msg: @autoclosure() -> String) /** Log warning messages. */ - func warn(module: String?, filename: String?, line: Int?, function: String?, @autoclosure msg: () -> String) + func warn(_ module: String?, filename: String?, line: Int?, function: String?, msg: @autoclosure() -> String) } extension OAuth2Logger { @@ -84,25 +84,25 @@ extension OAuth2Logger { The main log method, figures out whether to log the given message based on the receiver's logging level, then just uses `print`. Ignores filename, line and function. */ - public func log(atLevel: OAuth2LogLevel, module: String?, filename: String?, line: Int?, function: String?, @autoclosure msg: () -> String) { - if level != .Off && atLevel.rawValue >= level.rawValue { + public func log(_ atLevel: OAuth2LogLevel, module: String?, filename: String?, line: Int?, function: String?, msg: @autoclosure() -> String) { + if level != .off && atLevel.rawValue >= level.rawValue { print("[\(atLevel)] \(module ?? ""): \(msg())") } } /** Log a message at the trace level. */ - public func trace(module: String? = "OAuth2", filename: String? = #file, line: Int? = #line, function: String? = #function, @autoclosure msg: () -> String) { - log(.Trace, module: module, filename: filename, line: line, function: function, msg: msg) + 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) } /** Standard debug logging. */ - public func debug(module: String? = "OAuth2", filename: String? = #file, line: Int? = #line, function: String? = #function, @autoclosure msg: () -> String) { - log(.Debug, module: module, filename: filename, line: line, function: function, msg: msg) + 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 warning messages. */ - public func warn(module: String? = "OAuth2", filename: String? = #file, line: Int? = #line, function: String? = #function, @autoclosure msg: () -> String) { - log(.Warn, module: module, filename: filename, line: line, function: function, msg: msg) + 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) } } @@ -110,12 +110,12 @@ extension OAuth2Logger { /** Basic logger that just prints to stdout. */ -public class OAuth2DebugLogger: OAuth2Logger { +open class OAuth2DebugLogger: OAuth2Logger { /// The logger's logging level, set to `Debug` by default. - public var level = OAuth2LogLevel.Debug + open var level = OAuth2LogLevel.debug - public init(_ level: OAuth2LogLevel = OAuth2LogLevel.Debug) { + public init(_ level: OAuth2LogLevel = OAuth2LogLevel.debug) { self.level = level } } diff --git a/Sources/Base/OAuth2PasswordGrant.swift b/Sources/Base/OAuth2PasswordGrant.swift deleted file mode 100644 index b31d40c1..00000000 --- a/Sources/Base/OAuth2PasswordGrant.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// OAuth2PasswordGrant.swift -// OAuth2 -// -// Created by Tim Sneed on 6/5/15. -// Copyright (c) 2015 Pascal Pfiffner. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - - -/** - A class to handle authorization for clients via password grant. - */ -public class OAuth2PasswordGrant: OAuth2 { - - public override class var grantType: String { - return "password" - } - - /// Username to use during authentication. - public var username: String - - /// The user's password. - public var password: String - - /** - Adds support for the "password" & "username" setting. - */ - public override init(settings: OAuth2JSON) { - username = settings["username"] as? String ?? "" - password = settings["password"] as? String ?? "" - super.init(settings: settings) - } - - public override func doAuthorize(params params: [String : String]? = nil) { - self.obtainAccessToken(params: params) { params, error in - if let error = error { - self.didFail(error) - } - else { - self.didAuthorize(params ?? OAuth2JSON()) - } - } - } - - /** - Create a token request and execute it to receive an access token. - - - parameter callback: The callback to call after the request has returned - */ - func obtainAccessToken(params params: OAuth2StringDict? = nil, callback: ((params: OAuth2JSON?, error: ErrorType?) -> Void)) { - do { - let post = try tokenRequest(params: params).asURLRequestFor(self) - logger?.debug("OAuth2", msg: "Requesting new access token from \(post.URL?.description ?? "nil")") - - performRequest(post) { data, status, error in - do { - guard let data = data else { - throw error ?? OAuth2Error.NoDataInResponse - } - - let dict = try self.parseAccessTokenResponseData(data) - if status < 400 { - self.logger?.debug("OAuth2", msg: "Did get access token [\(nil != self.clientConfig.accessToken)]") - callback(params: dict, error: nil) - } - else { - callback(params: dict, error: OAuth2Error.ResponseError("The username or password is incorrect")) - } - } - catch let error { - self.logger?.debug("OAuth2", msg: "Error parsing response: \(error)") - callback(params: nil, error: error) - } - } - } - catch let err { - callback(params: nil, error: err) - } - } - - /** - Creates a POST request with x-www-form-urlencoded body created from the supplied URL's query part. - */ - func tokenRequest(params params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { - if username.isEmpty{ - throw OAuth2Error.NoUsername - } - if password.isEmpty{ - throw OAuth2Error.NoPassword - } - - let req = OAuth2AuthRequest(url: (clientConfig.tokenURL ?? clientConfig.authorizeURL)) - req.params["grant_type"] = self.dynamicType.grantType - req.params["username"] = username - req.params["password"] = password - if let clientId = clientConfig.clientId { - req.params["client_id"] = clientId - } - if let scope = clientConfig.scope { - req.params["scope"] = scope - } - req.addParams(params: params) - - return req - } -} - diff --git a/Sources/Base/OAuth2Request.swift b/Sources/Base/OAuth2Request.swift deleted file mode 100644 index 9a85d1b9..00000000 --- a/Sources/Base/OAuth2Request.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// OAuth2Request.swift -// OAuth2 -// -// Created by Pascal Pfiffner on 6/24/14. -// Copyright 2014 Pascal Pfiffner -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - - -/** - A request that can be signed by an OAuth2 instance. - */ -public class OAuth2Request: NSMutableURLRequest -{ - /** - Convenience initalizer to instantiate and sign a mutable URL request in one go. - */ - convenience init(URL: NSURL!, oauth: OAuth2, cachePolicy: NSURLRequestCachePolicy, timeoutInterval: NSTimeInterval) { - self.init(URL: URL, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval) - self.sign(oauth) - } - - /** - Signs the receiver by setting its "Authorization" header to "Bearer {token}". - - Will log an error if the OAuth2 instance does not have an access token! - */ - func sign(oauth: OAuth2) { - if let access = oauth.clientConfig.accessToken where !access.isEmpty { - self.setValue("Bearer \(access)", forHTTPHeaderField: "Authorization") - } - else { - NSLog("Cannot sign request, access token is empty") - } - } -} - - -/** - An NSURLSession delegate that allows you to use self-signed SSL certificates. - - Doing so is a REALLY BAD IDEA, even in development environments where you can use real, free certificates that are valid a few months. - Still, sometimes you'll have to do this so this class is provided, but DO NOT SUBMIT your app using self-signed SSL certs to the App - Store. You have been warned! - */ -public class OAuth2DebugURLSessionDelegate: NSObject, NSURLSessionDelegate -{ - /// The host to allow a self-signed SSL certificate for. - let host: String - - public init(host: String) { - self.host = host - } - - public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, - completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) { - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - if challenge.protectionSpace.host == host { - let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!) - completionHandler(.UseCredential, credential) - return - } - } - completionHandler(.CancelAuthenticationChallenge, nil) - } -} - diff --git a/Sources/Base/OAuth2RequestPerformer.swift b/Sources/Base/OAuth2RequestPerformer.swift new file mode 100644 index 00000000..7e00c8b5 --- /dev/null +++ b/Sources/Base/OAuth2RequestPerformer.swift @@ -0,0 +1,52 @@ +// +// OAuth2RequestPerformer.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 9/12/16. +// Copyright © 2016 Pascal Pfiffner. All rights reserved. +// + +import Foundation + + +public protocol OAuth2RequestPerformer { + + /** + This method should start executing the given request, returning a URLSessionTask if it chooses to do so. **You do not neet to call + `resume()` on this task**, it's supposed to already have started. It is being returned so you may be able to do additional stuff. + + - parameter request: An URLRequest object that provides the URL, cache policy, request type, body data or body stream, and so on. + - parameter completionHandler: The completion handler to call when the load request is complete. + - returns: An already running session task + */ + func perform(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask? +} + + +/** +Simple implementation of `OAuth2RequestPerformer`, using `URLSession.dataTask()` to perform requests. +*/ +open class OAuth2DataTaskRequestPerformer: OAuth2RequestPerformer { + + /// The URLSession that should be used. + public var session: URLSession + + public init(session: URLSession) { + self.session = session + } + + /** + This method should start executing the given request, returning a URLSessionTask if it chooses to do so. **You do not neet to call + `resume()` on this task**, it's supposed to already have started. It is being returned so you may be able to do additional stuff. + + - parameter request: An URLRequest object that provides the URL, cache policy, request type, body data or body stream, and so on. + - parameter completionHandler: The completion handler to call when the load request is complete. + - returns: An already running session data task + */ + open func perform(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask? { + let task = session.dataTask(with: request, completionHandler: completionHandler) + task.resume() + return task + } +} + diff --git a/Sources/Base/OAuth2Requestable.swift b/Sources/Base/OAuth2Requestable.swift new file mode 100644 index 00000000..e20b3bfc --- /dev/null +++ b/Sources/Base/OAuth2Requestable.swift @@ -0,0 +1,219 @@ +// +// OAuth2Requestable.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 6/2/15. +// Copyright 2015 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +/// Typealias to ease working with JSON dictionaries. +public typealias OAuth2JSON = [String: Any] + +/// Typealias to work with dictionaries full of strings. +public typealias OAuth2StringDict = [String: String] + +/// Typealias to work with headers. +public typealias OAuth2Headers = [String: String] + + +/** +Abstract base class for OAuth2 authorization as well as client registration classes. +*/ +open class OAuth2Requestable { + + /// Set to `true` to log all the things. `false` by default. Use `"verbose": bool` in settings or assign `logger` yourself. + open var verbose = false { + didSet { + logger = verbose ? OAuth2DebugLogger() : nil + } + } + + /// The logger being used. Auto-assigned to a debug logger if you set `verbose` to true or false. + open var logger: OAuth2Logger? + + + /** + Base initializer. + */ + public init(verbose: Bool) { + self.verbose = verbose + logger = verbose ? OAuth2DebugLogger() : nil + logger?.debug("OAuth2", msg: "Initialization finished") + } + + public init(logger: OAuth2Logger?) { + self.logger = logger + self.verbose = (nil != logger) + logger?.debug("OAuth2", msg: "Initialization finished") + } + + + // MARK: - Requests + + /// The instance's current session, creating one by the book if necessary. Defaults to using an ephemeral session, you can use + /// `sessionConfiguration` and/or `sessionDelegate` to affect how the session is configured. + open var session: URLSession { + if nil == _session { + let config = sessionConfiguration ?? URLSessionConfiguration.ephemeral + _session = URLSession(configuration: config, delegate: sessionDelegate, delegateQueue: nil) + } + return _session! + } + + /// The backing store for `session`. + private var _session: URLSession? { + didSet { + requestPerformer = nil + } + } + + /// The configuration to use when creating `session`. Uses an `+ephemeralSessionConfiguration()` if nil. + open var sessionConfiguration: URLSessionConfiguration? { + didSet { + _session = nil + } + } + + /// URL session delegate that should be used for the `NSURLSession` the instance uses for requests. + open var sessionDelegate: URLSessionDelegate? { + didSet { + _session = nil + } + } + + /// The instance's OAuth2RequestPerformer, defaults to using OAuth2DataTaskRequestPerformer which uses `URLSession.dataTask()`. + open var requestPerformer: OAuth2RequestPerformer? + + /** + Perform the supplied request and call the callback with the response JSON dict or an error. This method is intended for authorization + calls, not for data calls outside of the OAuth2 dance. + + This implementation uses the shared `NSURLSession` and executes a data task. If the server responds with an error, this will be + converted into an error according to information supplied in the response JSON (if availale). + + The callback returns a response object that is easy to use, like so: + + perform(request: req) { response in + do { + let data = try response.responseData() + // do what you must with `data` as Data and `response.response` as HTTPURLResponse + } + catch let error { + // the request failed because of `error` + } + } + + Easy, right? + + - parameter request: The request to execute + - parameter callback: The callback to call when the request completes/fails. Looks terrifying, see above on how to use it + */ + open func perform(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) { + self.logger?.trace("OAuth2", msg: "REQUEST\n\(request.debugDescription)\n---") + let performer = requestPerformer ?? OAuth2DataTaskRequestPerformer(session: session) + requestPerformer = performer + let task = performer.perform(request: request) { sessData, sessResponse, error in + self.abortableTask = nil + self.logger?.trace("OAuth2", msg: "RESPONSE\n\(sessResponse?.debugDescription ?? "no response")\n\n\(String(data: sessData ?? Data(), encoding: String.Encoding.utf8) ?? "no data")\n---") + let http = (sessResponse is HTTPURLResponse) ? (sessResponse as! HTTPURLResponse) : HTTPURLResponse(url: request.url!, statusCode: 499, httpVersion: nil, headerFields: nil)! + let response = OAuth2Response(data: sessData, request: request, response: http, error: error) + callback(response) + } + abortableTask = task + } + + /// Currently running abortable session task. + private var abortableTask: URLSessionTask? + + /** + Can be called to immediately abort the currently running authorization request, if it was started by `perform(request:callback:)`. + + - returns: A bool indicating whether a task was aborted or not + */ + func abortTask() -> Bool { + guard let task = abortableTask else { + return false + } + logger?.debug("OAuth2", msg: "Aborting request") + task.cancel() + return true + } + + + // MARK: - Utilities + + /** + Parse string-only JSON from NSData. + + - parameter data: NSData returned from the call, assumed to be JSON with string-values only. + - returns: An OAuth2JSON instance + */ + open func parseJSON(_ data: Data) throws -> OAuth2JSON { + do { + let json = try JSONSerialization.jsonObject(with: data, options: []) + if let json = json as? OAuth2JSON { + return json + } + if let str = String(data: data, encoding: String.Encoding.utf8) { + logger?.warn("OAuth2", msg: "JSON did not resolve to a dictionary, was: \(str)") + } + throw OAuth2Error.jsonParserError + } + catch let error where NSCocoaErrorDomain == error._domain && 3840 == error._code { // JSON parser error + if let str = String(data: data, encoding: String.Encoding.utf8) { + logger?.warn("OAuth2", msg: "Unparsable JSON was: \(str)") + } + throw OAuth2Error.jsonParserError + } + } + + /** + Parse a query string into a dictionary of String: String pairs. + + If you're retrieving a query or fragment from NSURLComponents, use the `percentEncoded##` variant as the others + automatically perform percent decoding, potentially messing with your query string. + + - parameter fromQuery: The query string you want to have parsed + - returns: A dictionary full of strings with the key-value pairs found in the query + */ + public final class func params(fromQuery query: String) -> OAuth2StringDict { + let parts = query.characters.split() { $0 == "&" }.map() { String($0) } + var params = OAuth2StringDict(minimumCapacity: parts.count) + for part in parts { + let subparts = part.characters.split() { $0 == "=" }.map() { String($0) } + if 2 == subparts.count { + params[subparts[0]] = subparts[1].wwwFormURLDecodedString + } + } + return params + } +} + + +/** +Helper function to ensure that the callback is executed on the main thread. +*/ +public func callOnMainThread(_ callback: ((Void) -> Void)) { + if Thread.isMainThread { + callback() + } + else { + DispatchQueue.main.sync(execute: callback) + } +} + diff --git a/Sources/Base/OAuth2Response.swift b/Sources/Base/OAuth2Response.swift new file mode 100644 index 00000000..d6f29d85 --- /dev/null +++ b/Sources/Base/OAuth2Response.swift @@ -0,0 +1,109 @@ +// +// OAuth2Response.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 9/12/16. +// Copyright © 2016 Pascal Pfiffner. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +/** +Encapsulates a URLResponse to a URLRequest. + +Instances of this class are returned from `OAuth2Requestable` calls, they can be used like so: + + perform(request: req) { response in + do { + let data = try response.responseData() + // do what you must with `data` as Data and `response.response` as HTTPURLResponse + } + catch let error { + // the request failed because of `error` + } + } +*/ +open class OAuth2Response { + + /// The data that was returned. + open var data: Data? + + /// The request that generated this response. + open var request: URLRequest + + /// The underlying HTTPURLResponse. + open var response: HTTPURLResponse + + /// Error reported by the underlying mechanisms. + open var error: Error? + + + public init(data: Data?, request: URLRequest, response: HTTPURLResponse, error: Error?) { + self.data = data + self.request = request + self.response = response + self.error = error + } + + + // MARK: - Response Check + + /** + Throws errors if something with the request went wrong, noop otherwise. You can use this to quickly figure out how to proceed in + request callbacks. + + If data is returned but the status code is >= 400, nothing will be raised **except** if there's a 401 or 403. + + - throws: Specific OAuth2Errors (.requestCancelled, .unauthorizedClient, .noDataInResponse) or any Error returned from the request + - returns: Response data + */ + open func responseData() throws -> Data { + if let error = error { + if NSURLErrorDomain == error._domain && -999 == error._code { // request was cancelled + throw OAuth2Error.requestCancelled + } + throw error + } + else if 401 == response.statusCode { + throw OAuth2Error.unauthorizedClient + } + else if 403 == response.statusCode { + throw OAuth2Error.forbidden + } + else if let data = data { + return data + } + else { + throw OAuth2Error.noDataInResponse + } + } + + /** + Uses `responseData()`, then decodes JSON using `Foundation.JSONSerialization` on the resulting data (if there was any). + + - throws: Any error thrown by `responseData()`, plus .jsonParserError if JSON did not decode into `[String: Any]` + - returns: OAuth2JSON on success + */ + open func responseJSON() throws -> OAuth2JSON { + let data = try responseData() + let json = try JSONSerialization.jsonObject(with: data, options: []) + if let json = json as? OAuth2JSON { + return json + } + throw OAuth2Error.jsonParserError + } +} + diff --git a/Sources/Base/OAuth2Securable.swift b/Sources/Base/OAuth2Securable.swift new file mode 100644 index 00000000..c72f4af8 --- /dev/null +++ b/Sources/Base/OAuth2Securable.swift @@ -0,0 +1,200 @@ +// +// OAuth2Requestable.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 6/2/15. +// Copyright 2015 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +/** +Base class to add keychain storage functionality. +*/ +open class OAuth2Securable: OAuth2Requestable { + + /// Server-side settings, as set upon initialization. + final let settings: OAuth2JSON + + /// If set to `true` (the default) will use system keychain to store tokens. Use `"keychain": bool` in settings. + public var useKeychain = true { + didSet { + if useKeychain { + updateFromKeychain() + } + } + } + + /// The keychain account to use to store tokens. Defaults to "currentTokens". + open var keychainAccountForTokens = "currentTokens" { + didSet { + assert(!keychainAccountForTokens.isEmpty) + } + } + + /// The keychain account name to use to store client credentials. Defaults to "clientCredentials". + open var keychainAccountForClientCredentials = "clientCredentials" { + didSet { + assert(!keychainAccountForClientCredentials.isEmpty) + } + } + + /// Defaults to `kSecAttrAccessibleWhenUnlocked`. MUST be set via `keychain_access_group` init setting. + open internal(set) var keychainAccessMode = kSecAttrAccessibleWhenUnlocked + + /// Keychain access group, none is set by default. MUST be set via `keychain_access_group` init setting. + open internal(set) var keychainAccessGroup: String? + + + /** + Base initializer. + + Looks at the `verbose`, `keychain`, `keychain_access_mode` and `keychain_access_group`. Everything else is handled by subclasses. + */ + public init(settings: OAuth2JSON) { + self.settings = settings + + // keychain settings + if let keychain = settings["keychain"] as? Bool { + useKeychain = keychain + } + if let accessMode = settings["keychain_access_mode"] as? String { + keychainAccessMode = accessMode as CFString + } + if let accessGroup = settings["keychain_access_group"] as? String { + keychainAccessGroup = accessGroup + } + + // logging settings + var verbose = false + if let verb = settings["verbose"] as? Bool { + verbose = verb + } + super.init(verbose: verbose) + + // init from keychain + if useKeychain { + updateFromKeychain() + } + } + + + // MARK: - Keychain Integration + + /** The service key under which to store keychain items. Returns "http://localhost", subclasses override to return the authorize URL. */ + open func keychainServiceName() -> String { + return "http://localhost" + } + + /** Queries the keychain for tokens stored for the receiver's authorize URL, and updates the token properties accordingly. */ + private func updateFromKeychain() { + logger?.debug("OAuth2", msg: "Looking for items in keychain") + + do { + var creds = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForClientCredentials) + let creds_data = try creds.fetchedFromKeychain() + updateFromKeychainItems(creds_data) + } + catch { + logger?.warn("OAuth2", msg: "Failed to load client credentials from keychain: \(error)") + } + + do { + var toks = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForTokens) + let toks_data = try toks.fetchedFromKeychain() + updateFromKeychainItems(toks_data) + } + catch { + logger?.warn("OAuth2", msg: "Failed to load tokens from keychain: \(error)") + } + } + + /** Updates instance properties according to the items found in the given dictionary, which was found in the keychain. */ + func updateFromKeychainItems(_ items: [String: Any]) { + } + + /** + Items that should be stored when storing client credentials. + + - returns: A dictionary with `String` keys and `Any` items + */ + open func storableCredentialItems() -> [String: Any]? { + return nil + } + + /** Stores our client credentials in the keychain. */ + open func storeClientToKeychain() { + if let items = storableCredentialItems() { + logger?.debug("OAuth2", msg: "Storing client credentials to keychain") + let keychain = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForClientCredentials, data: items) + do { + try keychain.saveInKeychain() + } + catch { + logger?.warn("OAuth2", msg: "Failed to store client credentials to keychain: \(error)") + } + } + } + + /** + Items that should be stored when tokens are stored to the keychain. + + - returns: A dictionary with `String` keys and `Any` items + */ + open func storableTokenItems() -> [String: Any]? { + return nil + } + + /** Stores our current token(s) in the keychain. */ + public func storeTokensToKeychain() { + if let items = storableTokenItems() { + logger?.debug("OAuth2", msg: "Storing tokens to keychain") + let keychain = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForTokens, data: items) + do { + try keychain.saveInKeychain() + } + catch let error { + logger?.warn("OAuth2", msg: "Failed to store tokens to keychain: \(error)") + } + } + } + + /** Unsets the client credentials and deletes them from the keychain. */ + open func forgetClient() { + logger?.debug("OAuth2", msg: "Forgetting client credentials and removing them from keychain") + let keychain = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForClientCredentials) + do { + try keychain.removeFromKeychain() + } + catch { + logger?.warn("OAuth2", msg: "Failed to delete credentials from keychain: \(error)") + } + } + + /** Unsets the tokens and deletes them from the keychain. */ + open func forgetTokens() { + logger?.debug("OAuth2", msg: "Forgetting tokens and removing them from keychain") + + let keychain = OAuth2KeychainAccount(oauth2: self, account: keychainAccountForTokens) + do { + try keychain.removeFromKeychain() + } + catch { + logger?.warn("OAuth2", msg: "Failed to delete tokens from keychain: \(error)") + } + } +} + diff --git a/Sources/Base/extensions.swift b/Sources/Base/extensions.swift index 8cccaf7e..67582a9a 100644 --- a/Sources/Base/extensions.swift +++ b/Sources/Base/extensions.swift @@ -21,12 +21,12 @@ import Foundation -extension NSHTTPURLResponse { +extension HTTPURLResponse { /// A localized string explaining the current `statusCode`. public var statusString: String { get { - return NSHTTPURLResponse.localizedStringForStatusCode(self.statusCode) + return HTTPURLResponse.localizedString(forStatusCode: self.statusCode) } } } @@ -34,63 +34,92 @@ extension NSHTTPURLResponse { extension String { - private static var wwwFormURLPlusSpaceCharacterSet: NSCharacterSet = NSMutableCharacterSet.wwwFormURLPlusSpaceCharacterSet() + fileprivate static var wwwFormURLPlusSpaceCharacterSet: CharacterSet = CharacterSet.wwwFormURLPlusSpaceCharacterSet /// Encodes a string to become x-www-form-urlencoded; the space is encoded as plus sign (+). var wwwFormURLEncodedString: String { let characterSet = String.wwwFormURLPlusSpaceCharacterSet - return (stringByAddingPercentEncodingWithAllowedCharacters(characterSet) ?? "").stringByReplacingOccurrencesOfString(" ", withString: "+") + return (addingPercentEncoding(withAllowedCharacters: characterSet) ?? "").replacingOccurrences(of: " ", with: "+") } /// Decodes a percent-encoded string and converts the plus sign into a space. var wwwFormURLDecodedString: String { - let rep = stringByReplacingOccurrencesOfString("+", withString: " ") - return rep.stringByRemovingPercentEncoding ?? rep + let rep = replacingOccurrences(of: "+", with: " ") + return rep.removingPercentEncoding ?? rep } } -extension NSMutableCharacterSet { +extension CharacterSet { /** - Return the character set that does NOT need percent-encoding for x-www-form-urlencoded requests INCLUDING SPACE. - YOU are responsible for replacing spaces " " with the plus sign "+". - - RFC3986 and the W3C spec are not entirely consistent, we're using W3C's spec which says: - http://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm + Return the character set that does NOT need percent-encoding for x-www-form-urlencoded requests INCLUDING SPACE. + YOU are responsible for replacing spaces " " with the plus sign "+". - > If the byte is 0x20 (U+0020 SPACE if interpreted as ASCII): - > - Replace the byte with a single 0x2B byte ("+" (U+002B) character if interpreted as ASCII). - > If the byte is in the range 0x2A (*), 0x2D (-), 0x2E (.), 0x30 to 0x39 (0-9), 0x41 to 0x5A (A-Z), 0x5F (_), - > 0x61 to 0x7A (a-z) - > - Leave byte as-is - */ - class func wwwFormURLPlusSpaceCharacterSet() -> NSMutableCharacterSet { - let set = NSMutableCharacterSet.alphanumericCharacterSet() - set.addCharactersInString("-._* ") + RFC3986 and the W3C spec are not entirely consistent, we're using W3C's spec which says: + http://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm + + > If the byte is 0x20 (U+0020 SPACE if interpreted as ASCII): + > - Replace the byte with a single 0x2B byte ("+" (U+002B) character if interpreted as ASCII). + > If the byte is in the range 0x2A (*), 0x2D (-), 0x2E (.), 0x30 to 0x39 (0-9), 0x41 to 0x5A (A-Z), 0x5F (_), + > 0x61 to 0x7A (a-z) + > - Leave byte as-is + */ + static var wwwFormURLPlusSpaceCharacterSet: CharacterSet { + var set = CharacterSet().union(CharacterSet.alphanumerics) + set.insert(charactersIn: "-._* ") return set } } -extension NSURLRequest { +extension URLRequest { /** A string describing the request, including headers and body. */ - override public var debugDescription: String { - var msg = "HTTP/1.1 \(HTTPMethod ?? "METHOD") \(URL?.description ?? "/")" + public var debugDescription: String { + var msg = "HTTP/1.1 \(httpMethod ?? "METHOD") \(url?.description ?? "/")" allHTTPHeaderFields?.forEach() { msg += "\n\($0): \($1)" } - if let data = HTTPBody, let body = NSString(data: data, encoding: NSUTF8StringEncoding) { + if let data = httpBody, let body = String(data: data, encoding: String.Encoding.utf8) { msg += "\n\n\(body)" } return msg } + + /** + Signs the receiver by setting its "Authorization" header to "Bearer {token}". + + Will log an error if the OAuth2 instance does not have an access token. + + - parameter oauth2: The OAuth2 instance providing the access token to sign the request + */ + public mutating func sign(with oauth2: OAuth2Base) { + if let access = oauth2.clientConfig.accessToken, !access.isEmpty { + setValue("Bearer \(access)", forHTTPHeaderField: "Authorization") + } + else { + NSLog("Cannot sign request, access token is empty") + } + } + + /** + Returns a copy of the receiver, signed by setting its "Authorization" header to "Bearer {token}". + + Will log an error if the OAuth2 instance does not have an access token. + + - parameter oauth2: The OAuth2 instance providing the access token to sign the receiver + */ + public func signed(with oauth2: OAuth2Base) -> URLRequest { + var signed = self + signed.sign(with: oauth2) + return signed + } } -extension NSHTTPURLResponse { +extension HTTPURLResponse { /** Format HTTP status and response headers as is customary. */ - override public var debugDescription: String { + override open var debugDescription: String { var msg = "HTTP/1.1 \(statusCode) \(statusString)" allHeaderFields.forEach() { msg += "\n\($0): \($1)" } return msg diff --git a/Sources/DataLoader/OAuth2DataLoader.swift b/Sources/DataLoader/OAuth2DataLoader.swift new file mode 100644 index 00000000..7d44c5a1 --- /dev/null +++ b/Sources/DataLoader/OAuth2DataLoader.swift @@ -0,0 +1,221 @@ +// +// OAuth2DataLoader.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 8/31/16. +// Copyright 2016 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +#if !NO_MODULE_IMPORT +import Base +import Flows +#endif + + +/** +A class that makes loading data from a protected endpoint easier. +*/ +open class OAuth2DataLoader: OAuth2Requestable { + + /// The OAuth2 instance used for OAuth2 access tokvarretrieval. + public let oauth2: OAuth2 + + /// If set to true, a 403 is treated as a 401. The default is false. + public var alsoIntercept403: Bool = false + + + public init(oauth2: OAuth2) { + self.oauth2 = oauth2 + super.init(logger: oauth2.logger) + } + + + // MARK: - Make Requests + + /// Our FIFO queue. + private var enqueued: [OAuth2DataRequest]? + + private var isAuthorizing = false + + /** + Overriding this method: it intercepts `unauthorizedClient` errors, stops and enqueues all calls, starts the authorization and, upon + success, resumes all enqueued calls. + + The callback is easy to use, like so: + + perform(request: req) { dataStatusResponse in + do { + let (data, status) = try dataStatusResponse() + // do what you must with `data` as Data and `status` as Int + } + catch let error { + // the request failed because of `error` + } + } + + - parameter request: The request to execute + - parameter callback: The callback to call when the request completes/fails. Looks terrifying, see above on how to use it + */ + override open func perform(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) { + perform(request: request, retry: true, callback: callback) + } + + /** + This method takes an additional `retry` flag, then uses the base implementation of `perform(request:callback:)` to perform the given + request. It intercepts 401 (and 403, if `alsoIntercept403` is true), enqueues the request and performs authorization. During + authorization, all requests to be performed are enqueued and they are all dequeued once authorization finishes, either by retrying + them on authorization success or by aborting them all with the same error. + + The callback is easy to use, like so: + + perform(request: req) { dataStatusResponse in + do { + let (data, status) = try dataStatusResponse() + // do what you must with `data` as Data and `status` as Int + } + catch let error { + // the request failed because of `error` + } + } + + - parameter request: The request to execute + - parameter retry: Whether the request should be retried on 401 (and possibly 403) + - parameter callback: The callback to call when the request completes/fails + */ + open func perform(request: URLRequest, retry: Bool, callback: @escaping ((OAuth2Response) -> Void)) { + guard !isAuthorizing else { + enqueue(request: request, callback: callback) + return + } + + super.perform(request: request) { response in + do { + if self.alsoIntercept403, 403 == response.response.statusCode { + throw OAuth2Error.unauthorizedClient + } + let _ = try response.responseData() + callback(response) + } + + // not authorized; stop and enqueue all requests, start authorization once, then re-try all enqueued requests + catch OAuth2Error.unauthorizedClient { + if retry { + self.enqueue(request: request, callback: callback) + self.attemptToAuthorize() { json, error in + + // dequeue all if we're authorized, throw all away if something went wrong + if nil != json { + self.retryAll() + } + else { + self.throwAllAway(with: error ?? OAuth2Error.requestCancelled) + } + } + } + else { + callback(response) + } + } + + // some other error, pass along + catch { + callback(response) + } + } + } + + /** + If not already authorizing, will use its `oauth2` instance to start authorization. + + This method will ignore calls while authorization is ongoing, meaning you will only get the callback once per authorization cycle. + + - parameter callback: The callback passed on from `authorize(callback:)`. Authorization finishes successfully (auth parameters will be + non-nil but may be an empty dict), fails (error will be non-nil) or is cancelled (both params and error are nil) + */ + open func attemptToAuthorize(callback: @escaping ((OAuth2JSON?, OAuth2Error?) -> Void)) { + if !isAuthorizing { + isAuthorizing = true + oauth2.authorize() { authParams, error in + self.isAuthorizing = false + callback(authParams, error) + } + } + } + + + // MARK: - Queue + + /** + Enqueues the given URLRequest and callback. You probably don't want to use this method yourself. + + - parameter request: The URLRequest to enqueue for later execution + - parameter callback: The closure to call when the request has been executed + */ + public func enqueue(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) { + enqueue(request: OAuth2DataRequest(request: request, callback: callback)) + } + + /** + Enqueues the given OAuth2DataRequest. You probably don't want to use this method yourself. + + - parameter request: The OAuth2DataRequest to enqueue for later execution + */ + public func enqueue(request: OAuth2DataRequest) { + if nil == enqueued { + enqueued = [request] + } + else { + enqueued!.append(request) + } + } + + /** + Dequeue all enqueued requests, applying the given closure to all of them. The queue will be empty by the time the closure is called. + + - parameter closure: The closure to apply to each enqueued request + */ + public func dequeueAndApply(closure: ((OAuth2DataRequest) -> Void)) { + guard let enq = enqueued else { + return + } + enqueued = nil + enq.forEach(closure) + } + + /** + Uses `dequeueAndApply()` by signing and re-performing all enqueued requests. + */ + func retryAll() { + dequeueAndApply() { req in + var request = req.request + request.sign(with: oauth2) + self.perform(request: request, retry: false, callback: req.callback) + } + } + + /** + Uses `dequeueAndApply()` to all enqueued requests, calling their callback with a response representing the given error. + + - parameter error: The error with which to finalize all enqueued requests + */ + func throwAllAway(with error: OAuth2Error) { + dequeueAndApply() { req in + let res = OAuth2Response(data: nil, request: req.request, response: HTTPURLResponse(), error: error) + req.callback(res) + } + } +} + diff --git a/Sources/DataLoader/OAuth2DataRequest.swift b/Sources/DataLoader/OAuth2DataRequest.swift new file mode 100644 index 00000000..1567b8a8 --- /dev/null +++ b/Sources/DataLoader/OAuth2DataRequest.swift @@ -0,0 +1,48 @@ +// +// OAuth2DataRequest.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 8/31/16. +// Copyright © 2016 Pascal Pfiffner. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +#if !NO_MODULE_IMPORT +import Base +import Flows +#endif + + +/** +A struct encapsulating an OAuth2 request made to obtain data. +*/ +public struct OAuth2DataRequest { + + /// The URLRequest to be executed. + public let request: URLRequest + + /// The callback executed when the request is done. + public let callback: (OAuth2Response) -> Void + + /// Any context to associate with the request. + public var context: Any? = nil + + + public init(request: URLRequest, callback: @escaping (OAuth2Response) -> Void) { + self.request = request + self.callback = callback + } +} + diff --git a/Sources/Flows/OAuth2.swift b/Sources/Flows/OAuth2.swift new file mode 100644 index 00000000..9b5b8064 --- /dev/null +++ b/Sources/Flows/OAuth2.swift @@ -0,0 +1,418 @@ +// +// OAuth2.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 6/4/14. +// Copyright 2014 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +#if !NO_MODULE_IMPORT + import Base + #if os(macOS) + import macOS + #elseif os(iOS) + import iOS + #elseif os(tvOS) + import tvOS + #endif +#endif + + +/** +Base class for specific OAuth2 flow implementations. +*/ +open class OAuth2: OAuth2Base { + + /// If non-nil, will be called before performing dynamic client registration, giving you a chance to instantiate your own registrar. + public final var onBeforeDynamicClientRegistration: ((URL) -> OAuth2DynReg?)? + + /// The authorizer to use for UI handling, depending on platform. + open var authorizer: OAuth2AuthorizerUI! + + + /** + Designated initializer. + + The following settings keys are currently supported: + + - client_id (string) + - 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 + - redirect_uris (list of URL-strings) + - scope (string) + + - client_name (string) + - registration_uri (URL-string) + - logo_uri (URL-string) + + - keychain (bool, true by default, applies to using the system keychain) + - keychain_access_mode (string, value for keychain kSecAttrAccessible attribute, kSecAttrAccessibleWhenUnlocked by default) + - keychain_access_group (string, value for keychain kSecAttrAccessGroup attribute, nil by default) + - verbose (bool, false by default, applies to client logging) + - secret_in_body (bool, false by default, forces the flow to use the request body for the client secret) + - token_assume_unexpired (bool, true by default, whether to use access tokens that do not come with an "expires_in" parameter) + */ + override public init(settings: OAuth2JSON) { + super.init(settings: settings) + authorizer = OAuth2Authorizer(oauth2: self) + } + + + // MARK: - Authorization + + /** + Use this method to obtain an access token. Take a look at `authConfig` on how to configure how authorization is presented to the user. + + This method is running asynchronously and can only be run one at a time. + + This method will first check if the client already has an unexpired access token (possibly from the keychain), if not and it's able to + use a refresh token it will try to use the refresh token. If this fails it will check whether the client has a client_id and show the + authorize screen if you have `authConfig` set up sufficiently. If `authConfig` is not set up sufficiently this method will end up + calling the `onFailure` callback. If client_id is not set but a "registration_uri" has been provided, a dynamic client registration will + be attempted and if it success, an access token will be requested. + + - parameter params: Optional key/value pairs to pass during authorization and token refresh + - parameter callback: The callback to call when authorization finishes (parameters will be non-nil but may be an empty dict), fails or is + cancelled (error will be non-nil, e.g. `.requestCancelled` if auth was aborted) + */ + public final func authorize(params: OAuth2StringDict? = nil, callback: @escaping ((OAuth2JSON?, OAuth2Error?) -> Void)) { + if isAuthorizing { + callback(nil, OAuth2Error.alreadyAuthorizing) + return + } + var prms = authParameters + if nil != prms, let params = params { + params.forEach() { prms![$0] = $1 } + } + let useParams = prms ?? params + + didAuthorizeOrFail = callback + logger?.debug("OAuth2", msg: "Starting authorization") + tryToObtainAccessTokenIfNeeded(params: useParams) { successParams in + if let successParams = successParams { + self.didAuthorize(withParameters: successParams) + } + else { + self.registerClientIfNeeded() { json, error in + if let error = error { + self.didFail(with: error) + } + else { + do { + assert(Thread.isMainThread) + try self.doAuthorize(params: useParams) + } + catch let error { + self.didFail(with: error.asOAuth2Error) + } + } + } + } + } + } + + /** + This method is deprecated in version 3.0 and has been replaced with `authorize(params:callback:)`. + + - parameter params: Optional key/value pairs to pass during authorization and token refresh + */ + @available(*, deprecated: 3.0, message: "Use the `authorize(params:callback:)` method and variants") + public final func authorize(params: OAuth2StringDict? = nil) { + authorize(params: params) { parameters, error in + } + } + + /** + Shortcut function to start embedded authorization from the given context (a UIViewController on iOS, an NSWindow on OS X). + + This method sets `authConfig.authorizeEmbedded = true` and `authConfig.authorizeContext = <# context #>`, then calls `authorize()` + + - parameter from: The context to start authorization from, depends on platform (UIViewController or NSWindow, see `authorizeContext`) + - parameter params: Optional key/value pairs to pass during authorization + - parameter callback: The callback to call when authorization finishes (parameters will be non-nil but may be an empty dict), fails or is + cancelled (error will be non-nil, e.g. `.requestCancelled` if auth was aborted) + */ + open func authorizeEmbedded(from context: AnyObject, params: OAuth2StringDict? = nil, callback: @escaping ((_ authParameters: OAuth2JSON?, _ error: OAuth2Error?) -> Void)) { + if isAuthorizing { // `authorize()` will check this, but we want to exit before changing `authConfig` + callback(nil, OAuth2Error.alreadyAuthorizing) + return + } + authConfig.authorizeEmbedded = true + authConfig.authorizeContext = context + authorize(params: params, callback: callback) + } + + /** + This method is deprecated in version 3.0 and has been replaced with `authorizeEmbedded(from:params:callback:)`. + + - parameter from: The context to start authorization from, depends on platform (UIViewController or NSWindow, see `authorizeContext`) + - parameter params: Optional key/value pairs to pass during authorization + */ + @available(*, deprecated: 3.0, message: "Use the `authorize(params:callback:)` method and variants") + open func authorizeEmbedded(from context: AnyObject, params: OAuth2StringDict? = nil) { + authorizeEmbedded(from: context, params: params) { parameters, error in + } + } + + /** + If the instance has an accessToken, checks if its expiry time has not yet passed. If we don't have an expiry date we assume the token + is still valid. + + - returns: A Bool indicating whether a probably valid access token exists + */ + open func hasUnexpiredAccessToken() -> Bool { + guard let access = accessToken, !access.isEmpty else { + return false + } + if let expiry = accessTokenExpiry { + return (.orderedDescending == expiry.compare(Date())) + } + return clientConfig.accessTokenAssumeUnexpired + } + + /** + Attempts to receive a new access token by: + + 1. checking if there still is an unexpired token + 2. attempting to use a refresh token + + Indicates, in the callback, whether the client has been able to obtain an access token that is likely to still work (but there is no + guarantee!) or not. + + - parameter params: Optional key/value pairs to pass during authorization + - parameter callback: The callback to call once the client knows whether it has an access token or not; if `success` is true an + access token is present + */ + open func tryToObtainAccessTokenIfNeeded(params: OAuth2StringDict? = nil, callback: @escaping ((OAuth2JSON?) -> Void)) { + if hasUnexpiredAccessToken() { + logger?.debug("OAuth2", msg: "Have an apparently unexpired access token") + callback(OAuth2JSON()) + } + else { + logger?.debug("OAuth2", msg: "No access token, checking if a refresh token is available") + doRefreshToken(params: params) { successParams, error in + if let successParams = successParams { + callback(successParams) + } + else { + if let err = error { + self.logger?.debug("OAuth2", msg: "\(err)") + } + callback(nil) + } + } + } + } + + /** + Method to actually start authorization. The public `authorize()` method only proceeds to this method if there is no valid access token + and if optional client registration succeeds. + + Can be overridden in subclasses to perform an authorization dance different from directing the user to a website. + + - parameter params: Optional key/value pairs to pass during authorization + */ + open func doAuthorize(params: OAuth2StringDict? = nil) throws { + if authConfig.authorizeEmbedded { + try doAuthorizeEmbedded(with: authConfig, params: params) + } + else { + try doOpenAuthorizeURLInBrowser(params: params) + } + } + + /** + Open the authorize URL in the OS's browser. Forwards to the receiver's `authorizer`, which is a platform-dependent implementation of + `OAuth2AuthorizerUI`. + + - parameter params: Additional parameters to pass to the authorize URL + - throws: UnableToOpenAuthorizeURL on failure + */ + final func doOpenAuthorizeURLInBrowser(params: OAuth2StringDict? = nil) throws { + let url = try authorizeURL(params: params) + logger?.debug("OAuth2", msg: "Opening authorize URL in system browser: \(url)") + try authorizer.openAuthorizeURLInBrowser(url) + } + + /** + Tries to use the current auth config context, which on iOS should be a UIViewController and on OS X a NSViewController, to present the + authorization screen. Set `oauth2.authConfig.authorizeContext` accordingly. + + Forwards to the receiver's `authorizer`, which is a platform-dependent implementation of `OAuth2AuthorizerUI`. + + - throws: Can throw OAuth2Error if the method is unable to show the authorize screen + - parameter with: The configuration to be used; usually uses the instance's `authConfig` + - parameter params: Additional authorization parameters to supply during the OAuth dance + */ + final func doAuthorizeEmbedded(with config: OAuth2AuthConfig, params: OAuth2StringDict? = nil) throws { + let url = try authorizeURL(params: params) + logger?.debug("OAuth2", msg: "Opening authorize URL embedded: \(url)") + try authorizer.authorizeEmbedded(with: config, at: url) + } + + /** + Method that creates the OAuth2AuthRequest instance used to create the authorize URL + + - parameter redirect: The redirect URI string to supply. If it is nil, the first value of the settings' `redirect_uris` entries is + used. Must be present in the end! + - parameter scope: The scope to request + - parameter params: Any additional parameters as dictionary with string keys and values that will be added to the query part + - returns: OAuth2AuthRequest to be used to call to the authorize endpoint + */ + func authorizeRequest(withRedirect redirect: String, scope: String?, params: OAuth2StringDict?) throws -> OAuth2AuthRequest { + guard let clientId = clientConfig.clientId, !clientId.isEmpty else { + throw OAuth2Error.noClientId + } + + let req = OAuth2AuthRequest(url: clientConfig.authorizeURL, method: .GET) + req.params["redirect_uri"] = redirect + req.params["client_id"] = clientId + req.params["state"] = context.state + if let scope = scope ?? clientConfig.scope { + req.params["scope"] = scope + } + if let responseType = type(of: self).responseType { + req.params["response_type"] = responseType + } + req.add(params: params) + + return req + } + + /** + Most convenient method if you want the authorize URL to be created as defined in your settings dictionary. + + - parameter params: Optional, additional URL params to supply to the request + - returns: NSURL to be used to start the OAuth dance + */ + open func authorizeURL(params: OAuth2StringDict? = nil) throws -> URL { + return try authorizeURL(withRedirect: nil, scope: nil, params: params) + } + + /** + Convenience method to be overridden by and used from subclasses. + + - parameter redirect: The redirect URI string to supply. If it is nil, the first value of the settings' `redirect_uris` entries is + used. Must be present in the end! + - parameter scope: The scope to request + - parameter params: Any additional parameters as dictionary with string keys and values that will be added to the query part + - returns: NSURL to be used to start the OAuth dance + */ + open func authorizeURL(withRedirect redirect: String?, scope: String?, params: OAuth2StringDict?) throws -> URL { + guard let redirect = (redirect ?? clientConfig.redirect) else { + throw OAuth2Error.noRedirectURL + } + let req = try authorizeRequest(withRedirect: redirect, scope: scope, params: params) + context.redirectURL = redirect + return try req.asURL() + } + + + // MARK: - Refresh Token + + /** + Generate the request to be used for token refresh when we have a refresh token. + + This will set "grant_type" to "refresh_token", add the refresh token, and take care of the remaining parameters. + + - parameter params: Additional parameters to pass during token refresh + - returns: An `OAuth2AuthRequest` instance that is configured for token refresh + */ + open func tokenRequestForTokenRefresh(params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { + guard let clientId = clientId, !clientId.isEmpty else { + throw OAuth2Error.noClientId + } + guard let refreshToken = clientConfig.refreshToken, !refreshToken.isEmpty else { + throw OAuth2Error.noRefreshToken + } + + let req = OAuth2AuthRequest(url: (clientConfig.tokenURL ?? clientConfig.authorizeURL)) + req.params["grant_type"] = "refresh_token" + req.params["refresh_token"] = refreshToken + req.params["client_id"] = clientId + req.add(params: params) + + return req + } + + /** + If there is a refresh token, use it to receive a fresh access token. + + - parameter params: Optional key/value pairs to pass during token refresh + - parameter callback: The callback to call after the refresh token exchange has finished + */ + open func doRefreshToken(params: OAuth2StringDict? = nil, callback: @escaping ((OAuth2JSON?, OAuth2Error?) -> Void)) { + do { + let post = try tokenRequestForTokenRefresh(params: params).asURLRequest(for: self) + logger?.debug("OAuth2", msg: "Using refresh token to receive access token from \(post.url?.description ?? "nil")") + + perform(request: post) { response in + do { + let data = try response.responseData() + let json = try self.parseRefreshTokenResponseData(data) + if response.response.statusCode >= 400 { + throw OAuth2Error.generic("Failed with status \(response.response.statusCode)") + } + self.logger?.debug("OAuth2", msg: "Did use refresh token for access token [\(nil != self.clientConfig.accessToken)]") + callback(json, nil) + } + catch let error { + self.logger?.debug("OAuth2", msg: "Error refreshing access token: \(error)") + callback(nil, error.asOAuth2Error) + } + } + } + catch let error { + callback(nil, error.asOAuth2Error) + } + } + + + // MARK: - Registration + + /** + Use OAuth2 dynamic client registration to register the client, if needed. + + Returns immediately if the receiver's `clientId` is nil (with error = nil) or if there is no registration URL (with error). Otherwise + calls `onBeforeDynamicClientRegistration()` -- if it is non-nil -- and uses the returned `OAuth2DynReg` instance -- if it is non-nil. + If both are nil, instantiates a blank `OAuth2DynReg` instead, then attempts client registration. + + - 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)) { + if nil != clientId { + callOnMainThread() { + callback(nil, nil) + } + } + else if let url = clientConfig.registrationURL { + let dynreg = onBeforeDynamicClientRegistration?(url as URL) ?? OAuth2DynReg() + dynreg.register(client: self) { json, error in + callOnMainThread() { + callback(json, error?.asOAuth2Error) + } + } + } + else { + callOnMainThread() { + callback(nil, OAuth2Error.noRegistrationURL) + } + } + } +} + diff --git a/Sources/Base/OAuth2ClientCredentials.swift b/Sources/Flows/OAuth2ClientCredentials.swift similarity index 52% rename from Sources/Base/OAuth2ClientCredentials.swift rename to Sources/Flows/OAuth2ClientCredentials.swift index abc26e9d..dc44e4ea 100644 --- a/Sources/Base/OAuth2ClientCredentials.swift +++ b/Sources/Flows/OAuth2ClientCredentials.swift @@ -19,77 +19,79 @@ // import Foundation +#if !NO_MODULE_IMPORT +import Base +#endif /** - Class to handle two-legged OAuth2 requests of the "client_credentials" type. - */ -public class OAuth2ClientCredentials: OAuth2 { +Class to handle two-legged OAuth2 requests of the "client_credentials" type. +*/ +open class OAuth2ClientCredentials: OAuth2 { - public override class var grantType: String { + override open class var grantType: String { return "client_credentials" } - public override func doAuthorize(params inParams: OAuth2StringDict? = nil) { - self.obtainAccessToken(inParams) { params, error in + override open func doAuthorize(params inParams: OAuth2StringDict? = nil) { + self.obtainAccessToken(params: inParams) { params, error in if let error = error { - self.didFail(error) + self.didFail(with: error.asOAuth2Error) } else { - self.didAuthorize(params ?? OAuth2JSON()) + self.didAuthorize(withParameters: params ?? OAuth2JSON()) } } } + /** + Creates a POST request with x-www-form-urlencoded body created from the supplied URL's query part. + */ + open func accessTokenRequest(params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { + guard let clientId = clientConfig.clientId, !clientId.isEmpty else { + throw OAuth2Error.noClientId + } + guard nil != clientConfig.clientSecret else { + throw OAuth2Error.noClientSecret + } + + let req = OAuth2AuthRequest(url: (clientConfig.tokenURL ?? clientConfig.authorizeURL)) + req.params["grant_type"] = type(of: self).grantType + if let scope = clientConfig.scope { + req.params["scope"] = scope + } + req.add(params: params) + + return req + } + /** Use the client credentials to retrieve a fresh access token. + Uses `accessTokenRequest(params:)` to create the request, which you can subclass to change implementation specifics. + - parameter callback: The callback to call after the process has finished */ - func obtainAccessToken(params: OAuth2StringDict? = nil, callback: ((params: OAuth2JSON?, error: ErrorType?) -> Void)) { + public func obtainAccessToken(params: OAuth2StringDict? = nil, callback: @escaping ((_ params: OAuth2JSON?, _ error: OAuth2Error?) -> Void)) { do { - let post = try tokenRequest(params).asURLRequestFor(self) - logger?.debug("OAuth2", msg: "Requesting new access token from \(post.URL?.description ?? "nil")") + let post = try accessTokenRequest(params: params).asURLRequest(for: self) + logger?.debug("OAuth2", msg: "Requesting new access token from \(post.url?.description ?? "nil")") - performRequest(post) { data, status, error in + perform(request: post) { response in do { - guard let data = data else { - throw error ?? OAuth2Error.NoDataInResponse - } - - let params = try self.parseAccessTokenResponseData(data) + let data = try response.responseData() + let params = try self.parseAccessTokenResponse(data: data) self.logger?.debug("OAuth2", msg: "Did get access token [\(nil != self.clientConfig.accessToken)]") - callback(params: params, error: nil) + callback(params, nil) } catch let error { - callback(params: nil, error: error) + callback(nil, error.asOAuth2Error) } } } catch let error { - callback(params: nil, error: error) + callback(nil, error.asOAuth2Error) } } - - /** - Creates a POST request with x-www-form-urlencoded body created from the supplied URL's query part. - */ - func tokenRequest(params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { - guard let clientId = clientConfig.clientId where !clientId.isEmpty else { - throw OAuth2Error.NoClientId - } - guard nil != clientConfig.clientSecret else { - throw OAuth2Error.NoClientSecret - } - - let req = OAuth2AuthRequest(url: (clientConfig.tokenURL ?? clientConfig.authorizeURL)) - req.params["grant_type"] = self.dynamicType.grantType - if let scope = clientConfig.scope { - req.params["scope"] = scope - } - req.addParams(params: params) - - return req - } } diff --git a/Sources/Base/OAuth2ClientCredentialsReddit.swift b/Sources/Flows/OAuth2ClientCredentialsReddit.swift similarity index 85% rename from Sources/Base/OAuth2ClientCredentialsReddit.swift rename to Sources/Flows/OAuth2ClientCredentialsReddit.swift index 82d0eb3f..54295be3 100644 --- a/Sources/Base/OAuth2ClientCredentialsReddit.swift +++ b/Sources/Flows/OAuth2ClientCredentialsReddit.swift @@ -18,6 +18,10 @@ // limitations under the License. // +#if !NO_MODULE_IMPORT +import Base +#endif + /** Enables Reddit's special client credentials flow for installed apps. @@ -29,7 +33,7 @@ https://github.com/reddit/reddit/wiki/OAuth2#application-only-oauth */ public class OAuth2ClientCredentialsReddit: OAuth2ClientCredentials { - public override class var grantType: String { + override public class var grantType: String { return "https://oauth.reddit.com/grants/installed_client" } @@ -43,19 +47,19 @@ public class OAuth2ClientCredentialsReddit: OAuth2ClientCredentials { - parameter settings: The authorization settings */ - public override init(settings: OAuth2JSON) { + override public init(settings: OAuth2JSON) { deviceId = settings["device_id"] as? String super.init(settings: settings) clientConfig.clientSecret = "" } /** Add `device_id` parameter to the request created by the superclass. */ - override func tokenRequest(params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { + override open func accessTokenRequest(params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { guard let device = deviceId else { - throw OAuth2Error.Generic("You must configure this flow with a `device_id` (via settings) or manually assign `deviceId`") + throw OAuth2Error.generic("You must configure this flow with a `device_id` (via settings) or manually assign `deviceId`") } - let req = try super.tokenRequest(params) + let req = try super.accessTokenRequest(params: params) req.params["device_id"] = device return req } diff --git a/Sources/Base/OAuth2CodeGrant.swift b/Sources/Flows/OAuth2CodeGrant.swift similarity index 57% rename from Sources/Base/OAuth2CodeGrant.swift rename to Sources/Flows/OAuth2CodeGrant.swift index 4a835bea..46e57d1f 100644 --- a/Sources/Base/OAuth2CodeGrant.swift +++ b/Sources/Flows/OAuth2CodeGrant.swift @@ -19,6 +19,9 @@ // import Foundation +#if !NO_MODULE_IMPORT +import Base +#endif /** @@ -28,13 +31,13 @@ This auth flow is designed for clients that are capable of protecting their clie exchange and token refresh flows, **if** the client has a secret, a "Basic key:secret" Authorization header will be used. If not the client key will be embedded into the request body. */ -public class OAuth2CodeGrant: OAuth2 { +open class OAuth2CodeGrant: OAuth2 { - public override class var grantType: String { + override open class var grantType: String { return "authorization_code" } - override public class var responseType: String? { + override open class var responseType: String? { return "code" } @@ -51,17 +54,17 @@ public class OAuth2CodeGrant: OAuth2 { - parameter params: Optional additional params to add as URL parameters - returns: A request you can use to create a URL request to exchange the code for an access token */ - func tokenRequestWithCode(code: String, params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { - guard let clientId = clientConfig.clientId where !clientId.isEmpty else { - throw OAuth2Error.NoClientId + open func accessTokenRequest(with code: String, params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { + guard let clientId = clientConfig.clientId, !clientId.isEmpty else { + throw OAuth2Error.noClientId } guard let redirect = context.redirectURL else { - throw OAuth2Error.NoRedirectURL + throw OAuth2Error.noRedirectURL } let req = OAuth2AuthRequest(url: (clientConfig.tokenURL ?? clientConfig.authorizeURL)) req.params["code"] = code - req.params["grant_type"] = self.dynamicType.grantType + req.params["grant_type"] = type(of: self).grantType req.params["redirect_uri"] = redirect req.params["client_id"] = clientId @@ -71,51 +74,48 @@ public class OAuth2CodeGrant: OAuth2 { /** Extracts the code from the redirect URL and exchanges it for a token. */ - override public func handleRedirectURL(redirect: NSURL) { + override open func handleRedirectURL(_ redirect: URL) { logger?.debug("OAuth2", msg: "Handling redirect URL \(redirect.description)") do { let code = try validateRedirectURL(redirect) exchangeCodeForToken(code) } catch let error { - didFail(error) + didFail(with: error.asOAuth2Error) } } /** Takes the received code and exchanges it for a token. + + Uses `accessTokenRequest(params:)` to create the request, which you can subclass to change implementation specifics. */ - public func exchangeCodeForToken(code: String) { + public func exchangeCodeForToken(_ code: String) { do { guard !code.isEmpty else { - throw OAuth2Error.PrerequisiteFailed("I don't have a code to exchange, let the user authorize first") + throw OAuth2Error.prerequisiteFailed("I don't have a code to exchange, let the user authorize first") } - let post = try tokenRequestWithCode(code).asURLRequestFor(self) - logger?.debug("OAuth2", msg: "Exchanging code \(code) for access token at \(post.URL!)") + let post = try accessTokenRequest(with: code).asURLRequest(for: self) + logger?.debug("OAuth2", msg: "Exchanging code \(code) for access token at \(post.url!)") - performRequest(post) { data, status, error in + perform(request: post) { response in do { - guard let data = data else { - throw error ?? OAuth2Error.NoDataInResponse - } - - let params = try self.parseAccessTokenResponseData(data) - if status < 400 { - self.logger?.debug("OAuth2", msg: "Did exchange code for access [\(nil != self.clientConfig.accessToken)] and refresh [\(nil != self.clientConfig.refreshToken)] tokens") - self.didAuthorize(params) - } - else { - throw OAuth2Error.Generic("\(status)") + let data = try response.responseData() + let params = try self.parseAccessTokenResponse(data: data) + if response.response.statusCode >= 400 { + throw OAuth2Error.generic("Failed with status \(response.response.statusCode)") } + self.logger?.debug("OAuth2", msg: "Did exchange code for access [\(nil != self.clientConfig.accessToken)] and refresh [\(nil != self.clientConfig.refreshToken)] tokens") + self.didAuthorize(withParameters: params) } catch let error { - self.didFail(error) + self.didFail(with: error.asOAuth2Error) } } } catch let error { - didFail(error) + didFail(with: error.asOAuth2Error) } } @@ -125,29 +125,26 @@ public class OAuth2CodeGrant: OAuth2 { /** Validates the redirect URI: returns a tuple with the code and nil on success, nil and an error on failure. */ - func validateRedirectURL(redirect: NSURL) throws -> String { + open func validateRedirectURL(_ redirect: URL) throws -> String { guard let expectRedirect = context.redirectURL else { - throw OAuth2Error.NoRedirectURL - } - guard let redir = redirect.absoluteString else { - throw OAuth2Error.InvalidRedirectURL(redirect.description) + throw OAuth2Error.noRedirectURL } - let comp = NSURLComponents(URL: redirect, resolvingAgainstBaseURL: true) - if !redir.hasPrefix(expectRedirect) && (!redir.hasPrefix("urn:ietf:wg:oauth:2.0:oob") && "localhost" != comp?.host) { - throw OAuth2Error.InvalidRedirectURL("Expecting «\(expectRedirect)» but received «\(redirect)»") + let comp = URLComponents(url: redirect, resolvingAgainstBaseURL: true) + if !(redirect.absoluteString.hasPrefix(expectRedirect)) && (!(redirect.absoluteString.hasPrefix("urn:ietf:wg:oauth:2.0:oob")) && "localhost" != comp?.host) { + throw OAuth2Error.invalidRedirectURL("Expecting «\(expectRedirect)» but received «\(redirect)»") } - if let compQuery = comp?.query where compQuery.characters.count > 0 { - let query = OAuth2CodeGrant.paramsFromQuery(comp!.percentEncodedQuery!) - try assureNoErrorInResponse(query) + if let compQuery = comp?.query, compQuery.characters.count > 0 { + let query = OAuth2CodeGrant.params(fromQuery: comp!.percentEncodedQuery!) + try assureNoErrorInResponse(query as OAuth2JSON) if let cd = query["code"] { // we got a code, use it if state is correct (and reset state) - try assureMatchesState(query) + try assureMatchesState(query as OAuth2JSON) return cd } - throw OAuth2Error.ResponseError("No “code” received") + throw OAuth2Error.responseError("No “code” received") } - throw OAuth2Error.PrerequisiteFailed("The redirect URL contains no query fragment") + throw OAuth2Error.prerequisiteFailed("The redirect URL contains no query fragment") } } diff --git a/Sources/Base/OAuth2CodeGrantBasicAuth.swift b/Sources/Flows/OAuth2CodeGrantBasicAuth.swift similarity index 64% rename from Sources/Base/OAuth2CodeGrantBasicAuth.swift rename to Sources/Flows/OAuth2CodeGrantBasicAuth.swift index 57e2f9a3..6499f28f 100644 --- a/Sources/Base/OAuth2CodeGrantBasicAuth.swift +++ b/Sources/Flows/OAuth2CodeGrantBasicAuth.swift @@ -19,16 +19,19 @@ // import Foundation +#if !NO_MODULE_IMPORT +import Base +#endif /** - Enhancing the code grant flow by allowing to specify a specific "Basic xx" authorization header. +Enhancing the code grant flow by allowing to specify a specific "Basic xx" authorization header. - This class allows you to manually set the "Authorization" header to a given string, as accepted in its `basicToken` property. It will - override the superclasses automatic generation of an Authorization header if the client has a clientSecret, so you only need to use - this subclass if you need a different header (this is different to version 1.2.3 and earlier of this framework). - */ -public class OAuth2CodeGrantBasicAuth: OAuth2CodeGrant { +This class allows you to manually set the "Authorization" header to a given string, as accepted in its `basicToken` property. It will +override the superclasses automatic generation of an Authorization header if the client has a clientSecret, so you only need to use +this subclass if you need a different header (this is different to version 1.2.3 and earlier of this framework). +*/ +open class OAuth2CodeGrantBasicAuth: OAuth2CodeGrant { /// The full token string to be used in the authorization header. var basicToken: String? @@ -38,7 +41,7 @@ public class OAuth2CodeGrantBasicAuth: OAuth2CodeGrant { - basic: takes precedence over client_id and client_secret for the token request Authorization header */ - public override init(settings: OAuth2JSON) { + override public init(settings: OAuth2JSON) { if let basic = settings["basic"] as? String { basicToken = basic } @@ -48,11 +51,11 @@ public class OAuth2CodeGrantBasicAuth: OAuth2CodeGrant { /** Calls super's implementation to obtain a token request, then adds the custom "Basic" authorization header. */ - override func tokenRequestWithCode(code: String, params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { - let req = try super.tokenRequestWithCode(code, params: params) + override open func accessTokenRequest(with code: String, params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { + let req = try super.accessTokenRequest(with: code, params: params) if let basic = basicToken { logger?.debug("OAuth2", msg: "Overriding “Basic” authorization header, as specified during client initialization") - req.headerAuthorize = "Basic \(basic)" + req.set(header: "Authorization", to: "Basic \(basic)") } else { logger?.warn("OAuth2", msg: "Using extended code grant, but “basicToken” is not actually specified. Using standard code grant.") diff --git a/Sources/Base/OAuth2CodeGrantFacebook.swift b/Sources/Flows/OAuth2CodeGrantFacebook.swift similarity index 65% rename from Sources/Base/OAuth2CodeGrantFacebook.swift rename to Sources/Flows/OAuth2CodeGrantFacebook.swift index c864cd63..90995b55 100644 --- a/Sources/Base/OAuth2CodeGrantFacebook.swift +++ b/Sources/Flows/OAuth2CodeGrantFacebook.swift @@ -19,24 +19,27 @@ // import Foundation +#if !NO_MODULE_IMPORT +import Base +#endif /** - Facebook only returns an "access_token=xyz&..." string, no true JSON, hence we override `parseTokenExchangeResponse` - and deal with the situation in a subclass. - */ +Facebook only returns an "access_token=xyz&..." string, no true JSON, hence we override `parseTokenExchangeResponse` +and deal with the situation in a subclass. +*/ public class OAuth2CodeGrantFacebook: OAuth2CodeGrant { /** Facebook doesn't return JSON but a plain URL-query-like string. This override takes care of the situation and extracts the token from the response. */ - override public func parseAccessTokenResponseData(data: NSData) throws -> OAuth2JSON { - guard let str = NSString(data: data, encoding: NSUTF8StringEncoding) as? String else { - throw OAuth2Error.UTF8DecodeError + override open func parseAccessTokenResponse(data: Data) throws -> OAuth2JSON { + guard let str = String(data: data, encoding: String.Encoding.utf8) else { + throw OAuth2Error.utf8DecodeError } - let query = self.dynamicType.paramsFromQuery(str) - return try parseAccessTokenResponse(query) + let query = type(of: self).params(fromQuery: str) + return try parseAccessTokenResponse(params: query) } } diff --git a/Sources/Base/OAuth2CodeGrantLinkedIn.swift b/Sources/Flows/OAuth2CodeGrantLinkedIn.swift similarity index 70% rename from Sources/Base/OAuth2CodeGrantLinkedIn.swift rename to Sources/Flows/OAuth2CodeGrantLinkedIn.swift index 254b295b..7df48763 100644 --- a/Sources/Base/OAuth2CodeGrantLinkedIn.swift +++ b/Sources/Flows/OAuth2CodeGrantLinkedIn.swift @@ -18,24 +18,28 @@ // limitations under the License. // +#if !NO_MODULE_IMPORT +import Base +#endif + /** - LinkedIn-specific subclass to deal with LinkedIn peculiarities: - - - Must have client-id/secret in request body - - Must use custom web view in order to be able to intercept http(s) redirects - - Will **not** return the "token_type" value, so must ignore it not being present - */ +LinkedIn-specific subclass to deal with LinkedIn peculiarities: + +- Must have client-id/secret in request body +- Must use custom web view in order to be able to intercept http(s) redirects +- Will **not** return the "token_type" value, so must ignore it not being present +*/ public class OAuth2CodeGrantLinkedIn: OAuth2CodeGrant { - public override init(settings: OAuth2JSON) { + override public init(settings: OAuth2JSON) { super.init(settings: settings) authConfig.secretInBody = true authConfig.authorizeEmbedded = true // necessary because only http(s) redirects are allowed authConfig.ui.useSafariView = false // must use custom web view in order to be able to intercept http(s) redirects } - override func assureCorrectBearerType(params: OAuth2JSON) throws { + override open func assureCorrectBearerType(_ params: OAuth2JSON) throws { } } diff --git a/Sources/Base/OAuth2CodeGrantNoTokenType.swift b/Sources/Flows/OAuth2CodeGrantNoTokenType.swift similarity index 85% rename from Sources/Base/OAuth2CodeGrantNoTokenType.swift rename to Sources/Flows/OAuth2CodeGrantNoTokenType.swift index 38c6fd5f..803c99fd 100644 --- a/Sources/Base/OAuth2CodeGrantNoTokenType.swift +++ b/Sources/Flows/OAuth2CodeGrantNoTokenType.swift @@ -18,16 +18,16 @@ // limitations under the License. // +#if !NO_MODULE_IMPORT +import Base +#endif + /** Subclass to deal with sites that don't return `token_type`, such as Instagram or Bitly. */ public class OAuth2CodeGrantNoTokenType: OAuth2CodeGrant { - public override init(settings: OAuth2JSON) { - super.init(settings: settings) - } - - override func assureCorrectBearerType(params: OAuth2JSON) throws { + override open func assureCorrectBearerType(_ params: OAuth2JSON) throws { } } diff --git a/Sources/Base/OAuth2DynReg.swift b/Sources/Flows/OAuth2DynReg.swift similarity index 61% rename from Sources/Base/OAuth2DynReg.swift rename to Sources/Flows/OAuth2DynReg.swift index c19d7717..001727ab 100644 --- a/Sources/Base/OAuth2DynReg.swift +++ b/Sources/Flows/OAuth2DynReg.swift @@ -19,30 +19,26 @@ // import Foundation - - -public enum OAuth2EndpointAuthMethod: String { - case None = "none" - case ClientSecretPost = "client_secret_post" - case ClientSecretBasic = "client_secret_basic" -} +#if !NO_MODULE_IMPORT +import Base +#endif /** - Class to handle OAuth2 Dynamic Client Registration. +Class to handle OAuth2 Dynamic Client Registration. - This is a lightweight class that uses a OAuth2 instance's settings when registering, only few settings are held by instances of this - class. Hence it's highly portable and can be instantiated when needed with ease. +This is a lightweight class that uses a OAuth2 instance's settings when registering, only few settings are held by instances of this class. +Hence it's highly portable and can be instantiated when needed with ease. - For the full OAuth2 Dynamic Client Registration spec see https://tools.ietf.org/html/rfc7591 - */ -public class OAuth2DynReg { +For the full OAuth2 Dynamic Client Registration spec see https://tools.ietf.org/html/rfc7591 +*/ +open class OAuth2DynReg { /// Additional HTTP headers to supply during registration. - public var extraHeaders: OAuth2StringDict? + open var extraHeaders: OAuth2StringDict? /// Whether registration should also allow refresh tokens. Defaults to true, making sure "refresh_token" grant type is being registered. - public var allowRefreshTokens = true + open var allowRefreshTokens = true public init() { } @@ -55,47 +51,49 @@ public class OAuth2DynReg { - parameter client: The client to register and update with client credentials, when successful - parameter callback: The callback to call when done with the registration response (JSON) and/or an error */ - public func registerClient(client: OAuth2, callback: ((json: OAuth2JSON?, error: ErrorType?) -> Void)) { + open func register(client: OAuth2, callback: @escaping ((_ json: OAuth2JSON?, _ error: OAuth2Error?) -> Void)) { do { - let req = try registrationRequest(client) - client.logger?.debug("OAuth2", msg: "Registering client at \(req.URL!) with scopes “\(client.scope ?? "(none)")”") - client.performRequest(req) { data, status, error in + let req = try registrationRequest(for: client) + client.logger?.debug("OAuth2", msg: "Registering client at \(req.url!) with scopes “\(client.scope ?? "(none)")”") + client.perform(request: req) { response in do { - guard let data = data else { - throw error ?? OAuth2Error.NoDataInResponse - } - - let dict = try self.parseRegistrationResponse(data, client: client) + let data = try response.responseData() + let dict = try self.parseRegistrationResponse(data: data, client: client) try client.assureNoErrorInResponse(dict) - if status >= 400 { - client.logger?.warn("OAuth2", msg: "Registration failed with \(status)") + if response.response.statusCode >= 400 { + client.logger?.warn("OAuth2", msg: "Registration failed with \(response.response.statusCode)") } else { - self.didRegisterWith(dict, client: client) + self.didRegisterWith(json: dict, client: client) } - callback(json: dict, error: nil) + callback(dict, nil) } catch let error { - callback(json: nil, error: error) + callback(nil, error.asOAuth2Error) } } } catch let error { - callback(json: nil, error: error) + callback(nil, error.asOAuth2Error) } } // MARK: - Registration Request - /** Returns a mutable URL request, set up to be used for registration: POST method, JSON body data. */ - public func registrationRequest(client: OAuth2) throws -> NSMutableURLRequest { + /** + Returns a URL request, set up to be used for registration: POST method, JSON body data. + + - parameter for: The OAuth2 client the request is built for + - returns: A URL request to be used for registration + */ + open func registrationRequest(for client: OAuth2) throws -> URLRequest { guard let registrationURL = client.clientConfig.registrationURL else { - throw OAuth2Error.NoRegistrationURL + throw OAuth2Error.noRegistrationURL } - let req = NSMutableURLRequest(URL: registrationURL) - req.HTTPMethod = "POST" + var req = URLRequest(url: registrationURL) + req.httpMethod = "POST" req.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") req.setValue("application/json", forHTTPHeaderField: "Accept") if let headers = extraHeaders { @@ -103,15 +101,15 @@ public class OAuth2DynReg { req.setValue(val, forHTTPHeaderField: key) } } - let body = registrationBody(client) + let body = registrationBody(for: client) client.logger?.debug("OAuth2", msg: "Registration parameters: \(body)") - req.HTTPBody = try NSJSONSerialization.dataWithJSONObject(body, options: []) + req.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) return req } /** The body data to use for registration. */ - public func registrationBody(client: OAuth2) -> OAuth2JSON { + open func registrationBody(for client: OAuth2) -> OAuth2JSON { var dict = OAuth2JSON() if let client = client.clientConfig.clientName { dict["client_name"] = client @@ -127,23 +125,23 @@ public class OAuth2DynReg { } // grant types, response types and auth method - var grant_types = [client.dynamicType.grantType] + var grant_types = [type(of: client).grantType] if allowRefreshTokens { grant_types.append("refresh_token") } dict["grant_types"] = grant_types - if let responseType = client.dynamicType.responseType { + if let responseType = type(of: client).responseType { dict["response_types"] = [responseType] } dict["token_endpoint_auth_method"] = client.clientConfig.endpointAuthMethod.rawValue return dict } - public func parseRegistrationResponse(data: NSData, client: OAuth2) throws -> OAuth2JSON { + open func parseRegistrationResponse(data: Data, client: OAuth2) throws -> OAuth2JSON { return try client.parseJSON(data) } - public func didRegisterWith(json: OAuth2JSON, client: OAuth2) { + open func didRegisterWith(json: OAuth2JSON, client: OAuth2) { if let id = json["client_id"] as? String { client.clientId = id client.logger?.debug("OAuth2", msg: "Did register with client-id “\(id)”, params: \(json)") @@ -153,8 +151,8 @@ public class OAuth2DynReg { } if let secret = json["client_secret"] as? String { client.clientSecret = secret - if let expires = json["client_secret_expires_at"] as? Double where 0 != expires { - client.logger?.debug("OAuth2", msg: "Client secret will expire on \(NSDate(timeIntervalSince1970: expires))") + if let expires = json["client_secret_expires_at"] as? Double, 0 != expires { + client.logger?.debug("OAuth2", msg: "Client secret will expire on \(Date(timeIntervalSince1970: expires))") } } if let methodName = json["token_endpoint_auth_method"] as? String, let method = OAuth2EndpointAuthMethod(rawValue: methodName) { diff --git a/Sources/Base/OAuth2ImplicitGrant.swift b/Sources/Flows/OAuth2ImplicitGrant.swift similarity index 61% rename from Sources/Base/OAuth2ImplicitGrant.swift rename to Sources/Flows/OAuth2ImplicitGrant.swift index 0cb8cf0a..4bd87563 100644 --- a/Sources/Base/OAuth2ImplicitGrant.swift +++ b/Sources/Flows/OAuth2ImplicitGrant.swift @@ -19,41 +19,44 @@ // import Foundation +#if !NO_MODULE_IMPORT +import Base +#endif /** Class to handle OAuth2 requests for public clients, such as distributed Mac/iOS Apps. */ -public class OAuth2ImplicitGrant: OAuth2 { +open class OAuth2ImplicitGrant: OAuth2 { - override public class var grantType: String { + override open class var grantType: String { return "implicit" } - override public class var responseType: String? { + override open class var responseType: String? { return "token" } - override public func handleRedirectURL(redirect: NSURL) { + override open func handleRedirectURL(_ redirect: URL) { logger?.debug("OAuth2", msg: "Handling redirect URL \(redirect.description)") do { // token should be in the URL fragment - let comp = NSURLComponents(URL: redirect, resolvingAgainstBaseURL: true) - guard let fragment = comp?.percentEncodedFragment where fragment.characters.count > 0 else { - throw OAuth2Error.InvalidRedirectURL(redirect.description) + let comp = URLComponents(url: redirect, resolvingAgainstBaseURL: true) + guard let fragment = comp?.percentEncodedFragment, fragment.characters.count > 0 else { + throw OAuth2Error.invalidRedirectURL(redirect.description) } - let params = self.dynamicType.paramsFromQuery(fragment) - let dict = try parseAccessTokenResponse(params) + let params = type(of: self).params(fromQuery: fragment) + let dict = try parseAccessTokenResponse(params: params) logger?.debug("OAuth2", msg: "Successfully extracted access token") - didAuthorize(dict) + didAuthorize(withParameters: dict) } catch let error { - didFail(error) + didFail(with: error.asOAuth2Error) } } - override public func assureAccessTokenParamsAreValid(params: OAuth2JSON) throws { + override open func assureAccessTokenParamsAreValid(_ params: OAuth2JSON) throws { try assureMatchesState(params) } } diff --git a/Sources/Flows/OAuth2PasswordGrant.swift b/Sources/Flows/OAuth2PasswordGrant.swift new file mode 100644 index 00000000..6af3ea4b --- /dev/null +++ b/Sources/Flows/OAuth2PasswordGrant.swift @@ -0,0 +1,127 @@ +// +// OAuth2PasswordGrant.swift +// OAuth2 +// +// Created by Tim Sneed on 6/5/15. +// Copyright (c) 2015 Pascal Pfiffner. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +#if !NO_MODULE_IMPORT +import Base +#endif + + +/** +A class to handle authorization for clients via password grant. +*/ +open class OAuth2PasswordGrant: OAuth2 { + + override open class var grantType: String { + return "password" + } + + /// Username to use during authorization. + open var username: String + + /// The user's password. + open var password: String + + /** + Adds support for the "password" & "username" setting. + */ + override public init(settings: OAuth2JSON) { + username = settings["username"] as? String ?? "" + password = settings["password"] as? String ?? "" + super.init(settings: settings) + } + + override open func doAuthorize(params: [String : String]? = nil) { + self.obtainAccessToken(params: params) { params, error in + if let error = error { + self.didFail(with: error) + } + else { + self.didAuthorize(withParameters: params ?? OAuth2JSON()) + } + } + } + + /** + Creates a POST request with x-www-form-urlencoded body created from the supplied URL's query part. + */ + open func accessTokenRequest(params: OAuth2StringDict? = nil) throws -> OAuth2AuthRequest { + if username.isEmpty{ + throw OAuth2Error.noUsername + } + if password.isEmpty{ + throw OAuth2Error.noPassword + } + + let req = OAuth2AuthRequest(url: (clientConfig.tokenURL ?? clientConfig.authorizeURL)) + req.params["grant_type"] = type(of: self).grantType + req.params["username"] = username + req.params["password"] = password + if let clientId = clientConfig.clientId { + req.params["client_id"] = clientId + } + if let scope = clientConfig.scope { + req.params["scope"] = scope + } + req.add(params: params) + + return req + } + + /** + Create a token request and execute it to receive an access token. + + Uses `accessTokenRequest(params:)` to create the request, which you can subclass to change implementation specifics. + + - parameter callback: The callback to call after the request has returned + */ + public func obtainAccessToken(params: OAuth2StringDict? = nil, callback: @escaping ((_ params: OAuth2JSON?, _ error: OAuth2Error?) -> Void)) { + do { + let post = try accessTokenRequest(params: params).asURLRequest(for: self) + logger?.debug("OAuth2", msg: "Requesting new access token from \(post.url?.description ?? "nil")") + + perform(request: post) { response in + do { + let data = try response.responseData() + let dict = try self.parseAccessTokenResponse(data: data) + if response.response.statusCode >= 400 { + throw OAuth2Error.generic("Failed with status \(response.response.statusCode)") + } + self.logger?.debug("OAuth2", msg: "Did get access token [\(nil != self.clientConfig.accessToken)]") + callback(dict, nil) + } + catch OAuth2Error.unauthorizedClient { // TODO: which one is it? + callback(nil, OAuth2Error.wrongUsernamePassword) + } + catch OAuth2Error.forbidden { // TODO: which one is it? + callback(nil, OAuth2Error.wrongUsernamePassword) + } + catch let error { + self.logger?.debug("OAuth2", msg: "Error obtaining access token: \(error)") + callback(nil, error.asOAuth2Error) + } + } + } + catch let error { + callback(nil, error.asOAuth2Error) + } + } +} + diff --git a/Sources/OSX/OAuth2+OSX.swift b/Sources/OSX/OAuth2+OSX.swift deleted file mode 100644 index 751c7932..00000000 --- a/Sources/OSX/OAuth2+OSX.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// OAuth2+OSX.swift -// OAuth2 -// -// Created by Pascal Pfiffner on 4/19/15. -// Copyright 2015 Pascal Pfiffner -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Cocoa - - -extension OAuth2 { - - /** - Uses `NSWorkspace` to open the authorize URL in the OS browser. - - - parameter params: Additional parameters to pass to the authorize URL - - throws: UnableToOpenAuthorizeURL on failure - */ - public final func openAuthorizeURLInBrowser(params: OAuth2StringDict? = nil) throws { - let url = try authorizeURL(params) - if !NSWorkspace.sharedWorkspace().openURL(url) { - throw OAuth2Error.UnableToOpenAuthorizeURL - } - } - - - // MARK: - Embedded View - - /** - Tries to use the given context, which on OS X should be a NSViewController, to present the authorization screen. - - You should use `authorizeEmbeddedFrom(<# NSWindow #>)` (to not use a sheet don't provide a window), use this method if you have specific - reasons. - - - parameter config: The configuration to be used; usually uses the instance's `authConfig` - - parameter params: Additional authorization parameters to supply during the OAuth dance - - throws: Can throw several OAuth2Error if the method is unable to show the authorize screen - */ - public func authorizeEmbeddedWith(config: OAuth2AuthConfig, params: OAuth2StringDict? = nil) throws { - guard #available(OSX 10.10, *) else { - throw OAuth2Error.Generic("Embedded authorizing is only available in OS X 10.10 and later") - } - - // present as sheet - if let window = config.authorizeContext as? NSWindow { - try authorizeEmbeddedFromWindow(window, config: config, params: params) - } - - // present in new window (or with custom block) - else { - try authorizeInNewWindow(config, params: params) - } - } - - /** - Presents a modal sheet from the given window. - - - parameter window: The window from which to present the sheet - - parameter config: The auth configuration to take into consideration - - parameter params: Additional parameters to pass to the authorize URL - - returns: The sheet that is being queued for presentation - */ - @available(OSX 10.10, *) - public func authorizeEmbeddedFromWindow(window: NSWindow, config: OAuth2AuthConfig, params: OAuth2StringDict? = nil) throws -> NSWindow { - let controller = try presentableAuthorizeViewController(params) - controller.willBecomeSheet = true - let sheet = windowControllerForViewController(controller, withConfiguration: config).window! - - if config.authorizeEmbeddedAutoDismiss { - internalAfterAuthorizeOrFailure = { wasFailure, error in - window.endSheet(sheet) - } - } - window.makeKeyAndOrderFront(nil) - window.beginSheet(sheet, completionHandler: nil) - - return sheet - } - - /** - Creates a new window, containing our `OAuth2WebViewController`, and centers it on the screen. - - - parameter config: The auth configuration to take into consideration - - parameter params: Additional parameters to pass to the authorize URL - */ - @available(OSX 10.10, *) - public func authorizeInNewWindow(config: OAuth2AuthConfig, params: OAuth2StringDict? = nil) throws { - let controller = try presentableAuthorizeViewController(params) - let windowController = windowControllerForViewController(controller, withConfiguration: config) - authConfig.ui.windowController = windowController - - if config.authorizeEmbeddedAutoDismiss { - internalAfterAuthorizeOrFailure = { wasFailure, error in - controller.view.window?.close() - self.authConfig.ui.windowController = nil - } - } - windowController.window?.center() - windowController.showWindow(nil) - } - - /** - Instantiates and configures an `OAuth2WebViewController`, ready to be used in a window. - - - parameter params: Additional parameters to pass to the authorize URL - - returns: A web view controller that you can present to the user for login - */ - @available(OSX 10.10, *) - public func presentableAuthorizeViewController(params: OAuth2StringDict? = nil) throws -> OAuth2WebViewController { - let url = try authorizeURL(params) - let controller = OAuth2WebViewController() - controller.startURL = url - controller.interceptURLString = redirect! - controller.onIntercept = { url in - do { - try self.handleRedirectURL(url) - return true - } - catch let error { - self.logger?.warn("OAuth2", msg: "Cannot intercept redirect URL: \(error)") - } - return false - } - controller.onWillCancel = { - self.didFail(nil) - } - return controller - } - - /** - Prepares a window controller with the given web view controller as content. - - - parameter controller: The web view controller to use as content - - parameter withConfiguration: The auth config to use - - returns: A window controller, ready to be presented - */ - @available(OSX 10.10, *) - func windowControllerForViewController(controller: OAuth2WebViewController, withConfiguration config: OAuth2AuthConfig) -> NSWindowController { - let rect = NSMakeRect(0, 0, OAuth2WebViewController.WebViewWindowWidth, OAuth2WebViewController.WebViewWindowHeight) - let window = NSWindow(contentRect: rect, styleMask: [.Titled, .Closable, .Resizable, .FullSizeContentView], backing: .Buffered, defer: false) - window.backgroundColor = NSColor.whiteColor() - window.movableByWindowBackground = true - window.titlebarAppearsTransparent = true - window.titleVisibility = .Hidden - window.animationBehavior = .AlertPanel - if let title = config.ui.title { - window.title = title - } - - let windowController = NSWindowController(window: window) - windowController.contentViewController = controller - - return windowController - } -} - diff --git a/Sources/SwiftKeychain/Keychain.swift b/Sources/SwiftKeychain/Keychain.swift new file mode 120000 index 00000000..aaed110b --- /dev/null +++ b/Sources/SwiftKeychain/Keychain.swift @@ -0,0 +1 @@ +../../SwiftKeychain/Keychain/Keychain.swift \ No newline at end of file diff --git a/Sources/iOS/OAuth2+iOS.swift b/Sources/iOS/OAuth2+iOS.swift deleted file mode 100644 index 556d532b..00000000 --- a/Sources/iOS/OAuth2+iOS.swift +++ /dev/null @@ -1,239 +0,0 @@ -// -// OAuth2+iOS.swift -// OAuth2 -// -// Created by Pascal Pfiffner on 4/19/15. -// Copyright 2015 Pascal Pfiffner -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import UIKit -import SafariServices - - -extension OAuth2 { - - /** - Uses `UIApplication` to open the authorize URL in iOS's browser. - - - parameter params: Additional parameters to pass to the authorize URL - - throws: UnableToOpenAuthorizeURL on failure - */ - public final func openAuthorizeURLInBrowser(params: OAuth2StringDict? = nil) throws { - let url = try authorizeURL(params) - logger?.debug("OAuth2", msg: "Opening authorize URL in system browser: \(url)") - if !UIApplication.sharedApplication().openURL(url) { - throw OAuth2Error.UnableToOpenAuthorizeURL - } - } - - - // MARK: - Authorize Embedded - - /** - Tries to use the current auth config context, which on iOS should be a UIViewController, to present the authorization screen. - - You should use `authorizeEmbeddedFrom(<# view controller #>)`; use this method if you have specific reasons. - - - throws: Can throw several OAuth2Error if the method is unable to show the authorize screen - - parameter config: The configuration to be used; usually uses the instance's `authConfig` - - parameter params: Additional authorization parameters to supply during the OAuth dance - */ - public func authorizeEmbeddedWith(config: OAuth2AuthConfig, params: OAuth2StringDict? = nil) throws { - if let controller = config.authorizeContext as? UIViewController { - if #available(iOS 9, *), config.ui.useSafariView { - let web = try authorizeSafariEmbeddedFromViewController(controller, params: params) - if config.authorizeEmbeddedAutoDismiss { - internalAfterAuthorizeOrFailure = { wasFailure, error in - web.dismissViewControllerAnimated(true, completion: nil) - } - } - return - } - let web = try authorizeEmbeddedFromViewController(controller, params: params) - if config.authorizeEmbeddedAutoDismiss { - internalAfterAuthorizeOrFailure = { wasFailure, error in - web.dismissViewControllerAnimated(true, completion: nil) - } - } - return - } - throw (nil == config.authorizeContext) ? OAuth2Error.NoAuthorizationContext : OAuth2Error.InvalidAuthorizationContext - } - - - // MARK: - Safari Web View Controller - - /** - Presents a Safari view controller from the supplied view controller, loading the authorize URL. - - 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 does NOT dismiss the view controller automatically, you probably want to do this in the `afterAuthorizeOrFailure` closure. - Simply call this method first, then call `dismissViewController()` on the returned web view controller instance in that closure. Or use - `authorizeEmbeddedWith()` which does all this automatically. - - - parameter controller: The view controller to use for presentation - - parameter params: Optional additional URL parameters - - returns: SFSafariViewController, being already presented automatically - */ - @available(iOS 9.0, *) - public func authorizeSafariEmbeddedFromViewController(controller: UIViewController, params: OAuth2StringDict? = nil) throws -> SFSafariViewController { - let url = try authorizeURL(params) - logger?.debug("OAuth2", msg: "Opening authorize URL in embedded Safari: \(url)") - return presentSafariViewFor(url, from: controller) - } - - /** - Presents a Safari view controller from the supplied view controller, loading the authorize URL. - - 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. - - Automatically intercepts the redirect URL and performs the token exchange. It does NOT however dismiss the web view controller - automatically, you probably want to do this in the `afterAuthorizeOrFailure` closure. Simply call this method first, then assign - that closure in which you call `dismissViewController()` on the returned web view controller instance. - - - parameter controller: The view controller to use for presentation - - parameter redirect: The redirect URL to use - - parameter scope: The scope to use - - parameter params: Optional additional URL parameters - - returns: SFSafariViewController, being already presented automatically - */ - @available(iOS 9.0, *) - public func authorizeSafariEmbeddedFromViewController(controller: UIViewController, redirect: String, scope: String, params: OAuth2StringDict? = nil) throws -> SFSafariViewController { - let url = try authorizeURLWithRedirect(redirect, scope: scope, params: params) - return presentSafariViewFor(url, from: controller) - } - - /** - Presents and returns a Safari view controller loading the given URL and intercepting the given URL. - - - returns: SFSafariViewController, embedded in a UINavigationController being presented automatically - */ - @available(iOS 9.0, *) - final func presentSafariViewFor(url: NSURL, from: UIViewController) -> SFSafariViewController { - let web = SFSafariViewController(URL: url) - web.title = authConfig.ui.title - - let delegate = OAuth2SFViewControllerDelegate(oauth: self) - web.delegate = delegate - authConfig.ui.safariViewDelegate = delegate - - from.presentViewController(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. - */ - @available(iOS 9.0, *) - func safariViewControllerDidCancel(safari: SFSafariViewController) { - authConfig.ui.safariViewDelegate = nil - didFail(nil) - } - - - // MARK: - Custom Web View Controller - - /** - Presents a web view controller, contained in a UINavigationController, on the supplied view controller and loads the authorize URL. - - Automatically intercepts the redirect URL and performs the token exchange. It does NOT however dismiss the web view controller - automatically, you probably want to do this in the `afterAuthorizeOrFailure` closure. Simply call this method first, then assign - that closure in which you call `dismissViewController()` on the returned web view controller instance. - - - parameter controller: The view controller to use for presentation - - parameter params: Optional additional URL parameters - - returns: OAuth2WebViewController, embedded in a UINavigationController being presented automatically - */ - public func authorizeEmbeddedFromViewController(controller: UIViewController, params: OAuth2StringDict? = nil) throws -> OAuth2WebViewController { - let url = try authorizeURL(params) - logger?.debug("OAuth2", msg: "Opening authorize URL in embedded browser: \(url)") - return presentAuthorizeViewFor(url, intercept: redirect!, from: controller) - } - - /** - Presents a web view controller, contained in a UINavigationController, on the supplied view controller and loads the authorize URL. - - Automatically intercepts the redirect URL and performs the token exchange. It does NOT however dismiss the web view controller - automatically, you probably want to do this in the `afterAuthorizeOrFailure` closure. Simply call this method first, then assign - that closure in which you call `dismissViewController()` on the returned web view controller instance. - - - parameter controller: The view controller to use for presentation - - parameter redirect: The redirect URL to use - - parameter scope: The scope to use - - parameter params: Optional additional URL parameters - - returns: OAuth2WebViewController, embedded in a UINavigationController being presented automatically - */ - public func authorizeEmbeddedFromViewController(controller: UIViewController, - redirect: String, - scope: String, - params: OAuth2StringDict? = nil) throws -> OAuth2WebViewController { - let url = try authorizeURLWithRedirect(redirect, scope: scope, params: params) - return presentAuthorizeViewFor(url, intercept: redirect, from: controller) - } - - /** - Presents and returns a web view controller loading the given URL and intercepting the given URL. - - - returns: OAuth2WebViewController, embedded in a UINavigationController being presented automatically - */ - final func presentAuthorizeViewFor(url: NSURL, intercept: String, from: UIViewController) -> OAuth2WebViewController { - let web = OAuth2WebViewController() - web.title = authConfig.ui.title - web.backButton = authConfig.ui.backButton as? UIBarButtonItem - web.startURL = url - web.interceptURLString = intercept - web.onIntercept = { url in - do { - try self.handleRedirectURL(url) - return true - } - catch let err { - self.logger?.warn("OAuth2", msg: "Cannot intercept redirect URL: \(err)") - } - return false - } - web.onWillDismiss = { didCancel in - if didCancel { - self.didFail(nil) - } - } - - let navi = UINavigationController(rootViewController: web) - from.presentViewController(navi, animated: true, completion: nil) - - return web - } -} - - -class OAuth2SFViewControllerDelegate: NSObject, SFSafariViewControllerDelegate { - - let oauth: OAuth2 - - init(oauth: OAuth2) { - self.oauth = oauth - } - - @available(iOS 9.0, *) - func safariViewControllerDidFinish(controller: SFSafariViewController) { - oauth.safariViewControllerDidCancel(controller) - } -} - diff --git a/Sources/iOS/OAuth2Authorizer+iOS.swift b/Sources/iOS/OAuth2Authorizer+iOS.swift new file mode 100644 index 00000000..bd14ccda --- /dev/null +++ b/Sources/iOS/OAuth2Authorizer+iOS.swift @@ -0,0 +1,198 @@ +// +// OAuth2+iOS.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 4/19/15. +// Copyright 2015 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#if os(iOS) + +import UIKit +import SafariServices +#if !NO_MODULE_IMPORT +import Base +#endif + + +public final class OAuth2Authorizer: OAuth2AuthorizerUI { + + /// The OAuth2 instance this authorizer belongs to. + public unowned let oauth2: OAuth2Base + + /// Used to store the `SFSafariViewControllerDelegate`. + var safariViewDelegate: AnyObject? + + + public init(oauth2: OAuth2) { + self.oauth2 = oauth2 + } + + + // MARK: - OAuth2AuthorizerUI + + /** + Uses `UIApplication` to open the authorize URL in iOS's browser. + + - parameter url: The authorize URL to open + - throws: UnableToOpenAuthorizeURL on failure + */ + public func openAuthorizeURLInBrowser(_ url: URL) throws { + if !UIApplication.shared.openURL(url) { + throw OAuth2Error.unableToOpenAuthorizeURL + } + } + + /** + Tries to use the current auth config context, which on iOS should be a UIViewController, to present the authorization screen. + + - throws: Can throw OAuth2Error if the method is unable to show the authorize screen + - parameter with: The configuration to be used; usually uses the instance's `authConfig` + - 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) + } + } + } + else { + let web = try authorizeEmbedded(from: controller, at: url) + if config.authorizeEmbeddedAutoDismiss { + oauth2.internalAfterAuthorizeOrFail = { wasFailure, error in + web.dismiss(animated: true) + } + } + } + } + + + // MARK: - Safari Web View Controller + + /** + Presents a Safari view controller from the supplied view controller, loading the authorize URL. + + 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 does NOT dismiss the view controller automatically, you probably want to do this in the callback. + Simply call this method first, then call `dismissViewController()` on the returned web view controller instance in that closure. Or use + `authorizeEmbedded(with:at:)` which does all this automatically. + + - parameter from: The view controller to use for presentation + - parameter at: The authorize URL to open + - returns: SFSafariViewController, being already presented automatically + */ + @available(iOS 9.0, *) + @discardableResult + public func authorizeSafariEmbedded(from controller: UIViewController, at url: URL) throws -> SFSafariViewController { + let web = SFSafariViewController(url: url) + web.title = oauth2.authConfig.ui.title + + safariViewDelegate = OAuth2SFViewControllerDelegate(authorizer: self) + web.delegate = safariViewDelegate as! OAuth2SFViewControllerDelegate + + 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. + */ + @available(iOS 9.0, *) + func safariViewControllerDidCancel(_ safari: SFSafariViewController) { + safariViewDelegate = nil + oauth2.didFail(with: nil) + } + + + // MARK: - Custom Web View Controller + + /** + Presents a web view controller, contained in a UINavigationController, on the supplied view controller and loads the authorize URL. + + Automatically intercepts the redirect URL and performs the token exchange. It does NOT however dismiss the web view controller + automatically, you probably want to do this in the callback. Simply call this method first, then assign that closure in which you call + `dismissViewController()` on the returned web view controller instance. + + - parameter from: The view controller to use for presentation + - parameter at: The authorize URL to open + - returns: OAuth2WebViewController, embedded in a UINavigationController being presented automatically + */ + public func authorizeEmbedded(from controller: UIViewController, at url: URL) throws -> OAuth2WebViewController { + guard let redirect = oauth2.redirect else { + throw OAuth2Error.noRedirectURL + } + return presentAuthorizeView(forURL: url, intercept: redirect, from: controller) + } + + /** + Presents and returns a web view controller loading the given URL and intercepting the given URL. + + - returns: OAuth2WebViewController, embedded in a UINavigationController being presented automatically + */ + final func presentAuthorizeView(forURL url: URL, intercept: String, from: UIViewController) -> OAuth2WebViewController { + let web = OAuth2WebViewController() + web.title = oauth2.authConfig.ui.title + web.backButton = oauth2.authConfig.ui.backButton as? UIBarButtonItem + web.startURL = url + web.interceptURLString = intercept + web.onIntercept = { url in + do { + try self.oauth2.handleRedirectURL(url as URL) + return true + } + catch let err { + self.oauth2.logger?.warn("OAuth2", msg: "Cannot intercept redirect URL: \(err)") + } + return false + } + web.onWillDismiss = { didCancel in + if didCancel { + self.oauth2.didFail(with: nil) + } + } + + let navi = UINavigationController(rootViewController: web) + from.present(navi, animated: true) + + return web + } +} + + +class OAuth2SFViewControllerDelegate: NSObject, SFSafariViewControllerDelegate { + + let authorizer: OAuth2Authorizer + + init(authorizer: OAuth2Authorizer) { + self.authorizer = authorizer + } + + @available(iOS 9.0, *) + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + authorizer.safariViewControllerDidCancel(controller) + } +} + +#endif diff --git a/Sources/iOS/OAuth2WebViewController.swift b/Sources/iOS/OAuth2WebViewController.swift index 2615a56d..976c408c 100644 --- a/Sources/iOS/OAuth2WebViewController.swift +++ b/Sources/iOS/OAuth2WebViewController.swift @@ -17,23 +17,27 @@ // See the License for the specific language governing permissions and // limitations under the License. // +#if os(iOS) import UIKit +#if !NO_MODULE_IMPORT +import Base +#endif /** - A simple iOS web view controller that allows you to display the login/authorization screen. - */ -public class OAuth2WebViewController: UIViewController, UIWebViewDelegate -{ +A simple iOS web view controller that allows you to display the login/authorization screen. +*/ +open class OAuth2WebViewController: UIViewController, UIWebViewDelegate { + /// Handle to the OAuth2 instance in play, only used for debug lugging at this time. var oauth: OAuth2? /// The URL to load on first show. - public var startURL: NSURL? { + open var startURL: URL? { didSet(oldURL) { - if nil != startURL && nil == oldURL && isViewLoaded() { - loadURL(startURL!) + if nil != startURL && nil == oldURL && isViewLoaded { + load(url: startURL!) } } } @@ -42,8 +46,8 @@ public class OAuth2WebViewController: UIViewController, UIWebViewDelegate var interceptURLString: String? { didSet(oldURL) { if nil != interceptURLString { - if let url = NSURL(string: interceptURLString!) { - interceptComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: true) + if let url = URL(string: interceptURLString!) { + interceptComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) } else { oauth?.logger?.debug("OAuth2", msg: "Failed to parse URL \(interceptURLString), discarding") @@ -55,17 +59,17 @@ public class OAuth2WebViewController: UIViewController, UIWebViewDelegate } } } - var interceptComponents: NSURLComponents? + var interceptComponents: URLComponents? /// Closure called when the web view gets asked to load the redirect URL, specified in `interceptURLString`. Return a Bool indicating /// that you've intercepted the URL. - var onIntercept: ((url: NSURL) -> Bool)? + var onIntercept: ((URL) -> Bool)? - /// Called when the web view is about to be dismissed. - var onWillDismiss: ((didCancel: Bool) -> Void)? + /// Called when the web view is about to be dismissed. The Bool indicates whether the request was (user-)cancelled. + var onWillDismiss: ((_ didCancel: Bool) -> Void)? /// Assign to override the back button, shown when it's possible to go back in history. Will adjust target/action accordingly. - public var backButton: UIBarButtonItem? { + open var backButton: UIBarButtonItem? { didSet { if let backButton = backButton { backButton.target = self @@ -93,15 +97,15 @@ public class OAuth2WebViewController: UIViewController, UIWebViewDelegate // MARK: - View Handling - override public func loadView() { - edgesForExtendedLayout = .All + override open func loadView() { + edgesForExtendedLayout = .all extendedLayoutIncludesOpaqueBars = true automaticallyAdjustsScrollViewInsets = true super.loadView() - view.backgroundColor = UIColor.whiteColor() + view.backgroundColor = UIColor.white - cancelButton = UIBarButtonItem(barButtonSystemItem: .Cancel, target: self, action: #selector(OAuth2WebViewController.cancel(_:))) + cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(OAuth2WebViewController.cancel(_:))) navigationItem.rightBarButtonItem = cancelButton // create a web view @@ -112,17 +116,17 @@ public class OAuth2WebViewController: UIViewController, UIWebViewDelegate view.addSubview(web) let views = ["web": web] - view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[web]|", options: [], metrics: nil, views: views)) - view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[web]|", options: [], metrics: nil, views: views)) + view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[web]|", options: [], metrics: nil, views: views)) + view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[web]|", options: [], metrics: nil, views: views)) webView = web } - override public func viewWillAppear(animated: Bool) { + override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let web = webView where !web.canGoBack { + if let web = webView, !web.canGoBack { if nil != startURL { - loadURL(startURL!) + load(url: startURL!) } else { web.loadHTMLString("There is no `startURL`", baseURL: nil) @@ -130,9 +134,9 @@ public class OAuth2WebViewController: UIViewController, UIWebViewDelegate } } - func showHideBackButton(show: Bool) { + func showHideBackButton(_ show: Bool) { if show { - let bb = backButton ?? UIBarButtonItem(barButtonSystemItem: .Rewind, target: self, action: #selector(OAuth2WebViewController.goBack(_:))) + let bb = backButton ?? UIBarButtonItem(barButtonSystemItem: .rewind, target: self, action: #selector(OAuth2WebViewController.goBack(_:))) navigationItem.leftBarButtonItem = bb } else { @@ -148,73 +152,73 @@ public class OAuth2WebViewController: UIViewController, UIWebViewDelegate // TODO: implement } - func showErrorMessage(message: String, animated: Bool) { + func showErrorMessage(_ message: String, animated: Bool) { NSLog("Error: \(message)") } // MARK: - Actions - public func loadURL(url: NSURL) { - webView?.loadRequest(NSURLRequest(URL: url)) + open func load(url: URL) { + webView?.loadRequest(URLRequest(url: url)) } - func goBack(sender: AnyObject?) { + func goBack(_ sender: AnyObject?) { webView?.goBack() } - func cancel(sender: AnyObject?) { - dismiss(asCancel: true, animated: nil != sender ? true : false) + func cancel(_ sender: AnyObject?) { + dismiss(asCancel: true, animated: (nil != sender) ? true : false) } - func dismiss(animated animated: Bool) { - dismiss(asCancel: false, animated: animated) + override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + dismiss(asCancel: false, animated: flag, completion: completion) } - func dismiss(asCancel asCancel: Bool, animated: Bool) { + func dismiss(asCancel: Bool, animated: Bool, completion: (() -> Void)? = nil) { webView?.stopLoading() if nil != self.onWillDismiss { - self.onWillDismiss!(didCancel: asCancel) + self.onWillDismiss!(asCancel) } - dismissViewControllerAnimated(animated, completion: nil) + super.dismiss(animated: animated, completion: completion) } // MARK: - Web View Delegate - public func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { - if nil == onIntercept { + open func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { + guard let onIntercept = onIntercept else { return true } // we compare the scheme and host first, then check the path (if there is any). Not sure if a simple string comparison // would work as there may be URL parameters attached - if let url = request.URL where url.scheme == interceptComponents?.scheme && url.host == interceptComponents?.host { - let haveComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: true) - if let hp = haveComponents?.path, ip = interceptComponents?.path where hp == ip || ("/" == hp + ip) { - return !onIntercept!(url: url) + if let url = request.url, url.scheme == interceptComponents?.scheme && url.host == interceptComponents?.host { + let haveComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) + if let hp = haveComponents?.path, let ip = interceptComponents?.path, hp == ip || ("/" == hp + ip) { + return !onIntercept(url) } } return true } - public func webViewDidStartLoad(webView: UIWebView) { - if "file" != webView.request?.URL?.scheme { + open func webViewDidStartLoad(_ webView: UIWebView) { + if "file" != webView.request?.url?.scheme { showLoadingIndicator() } } /* Special handling for Google's `urn:ietf:wg:oauth:2.0:oob` callback */ - public func webViewDidFinishLoad(webView: UIWebView) { - if let scheme = interceptComponents?.scheme where "urn" == scheme { - if let path = interceptComponents?.path where path.hasPrefix("ietf:wg:oauth:2.0:oob") { - if let title = webView.stringByEvaluatingJavaScriptFromString("document.title") where title.hasPrefix("Success ") { + open func webViewDidFinishLoad(_ webView: UIWebView) { + if let scheme = interceptComponents?.scheme, "urn" == scheme { + if let path = interceptComponents?.path, path.hasPrefix("ietf:wg:oauth:2.0:oob") { + if let title = webView.stringByEvaluatingJavaScript(from: "document.title"), title.hasPrefix("Success ") { oauth?.logger?.debug("OAuth2", msg: "Creating redirect URL from document.title") - let qry = title.stringByReplacingOccurrencesOfString("Success ", withString: "") - if let url = NSURL(string: "http://localhost/?\(qry)") { - onIntercept?(url: url) + let qry = title.replacingOccurrences(of: "Success ", with: "") + if let url = URL(string: "http://localhost/?\(qry)") { + _ = onIntercept?(url) return } else { @@ -228,15 +232,16 @@ public class OAuth2WebViewController: UIViewController, UIWebViewDelegate showHideBackButton(webView.canGoBack) } - public func webView(webView: UIWebView, didFailLoadWithError error: NSError) { - if NSURLErrorDomain == error.domain && NSURLErrorCancelled == error.code { + open func webView(_ webView: UIWebView, didFailLoadWithError error: Error) { + if NSURLErrorDomain == error._domain && NSURLErrorCancelled == error._code { return } // do we still need to intercept "WebKitErrorDomain" error 102? if nil != loadingView { - showErrorMessage(error.localizedDescription ?? "Unknown web view load error", animated: true) + showErrorMessage(error.localizedDescription, animated: true) } } } +#endif diff --git a/Sources/macOS/OAuth2Authorizer+macOS.swift b/Sources/macOS/OAuth2Authorizer+macOS.swift new file mode 100644 index 00000000..29019bd3 --- /dev/null +++ b/Sources/macOS/OAuth2Authorizer+macOS.swift @@ -0,0 +1,187 @@ +// +// OAuth2Authorizer+macOS.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 4/19/15. +// Copyright 2015 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#if os(macOS) + +import Cocoa +#if !NO_MODULE_IMPORT +import Base +#endif + + +public final class OAuth2Authorizer: OAuth2AuthorizerUI { + + /// The OAuth2 instance this authorizer belongs to. + public unowned let oauth2: OAuth2Base + + /// Stores the default `NSWindowController` created to contain the web view controller. + var windowController: NSWindowController? + + + public init(oauth2: OAuth2Base) { + self.oauth2 = oauth2 + } + + + // MARK: - OAuth2AuthorizerUI + + + /** + Uses `NSWorkspace` to open the authorize URL in the OS browser. + + - parameter url: The authorize URL to open + - throws: UnableToOpenAuthorizeURL on failure + */ + public func openAuthorizeURLInBrowser(_ url: URL) throws { + if !NSWorkspace.shared().open(url) { + throw OAuth2Error.unableToOpenAuthorizeURL + } + } + + /** + Tries to use the given context, which on OS X should be a NSWindow, to present the authorization screen. In this case will forward to + `authorizeEmbedded(from:with:at:)`, if the context is empty will create a new NSWindow by calling `authorizeInNewWindow(with:at:)`. + + - parameter with: The configuration to be used; usually uses the instance's `authConfig` + - parameter at: The authorize URL to open + - throws: Can throw OAuth2Error if the method is unable to show the authorize screen + */ + public func authorizeEmbedded(with config: OAuth2AuthConfig, at url: URL) throws { + guard #available(macOS 10.10, *) else { + throw OAuth2Error.generic("Embedded authorizing is only available in OS X 10.10 and later") + } + + // present as sheet + if let window = config.authorizeContext as? NSWindow { + let sheet = try authorizeEmbedded(from: window, at: url) + if config.authorizeEmbeddedAutoDismiss { + oauth2.internalAfterAuthorizeOrFail = { wasFailure, error in + window.endSheet(sheet) + } + } + } + + // present in new window (or with custom block) + else { + windowController = try authorizeInNewWindow(at: url) + if config.authorizeEmbeddedAutoDismiss { + oauth2.internalAfterAuthorizeOrFail = { wasFailure, error in + self.windowController?.window?.close() + self.windowController = nil + } + } + } + } + + + // MARK: - Window Creation + + /** + Presents a modal sheet from the given window. + + - parameter from: The window from which to present the sheet + - parameter at: The authorize URL to open + - returns: The sheet that is being queued for presentation + */ + @available(macOS 10.10, *) + @discardableResult + public func authorizeEmbedded(from window: NSWindow, at url: URL) throws -> NSWindow { + let controller = try presentableAuthorizeViewController(at: url) + controller.willBecomeSheet = true + let sheet = windowController(forViewController: controller, with: oauth2.authConfig).window! + + window.makeKeyAndOrderFront(nil) + window.beginSheet(sheet, completionHandler: nil) + + return sheet + } + + /** + Creates a new window, containing our `OAuth2WebViewController`, and centers it on the screen. + + - parameter at: The authorize URL to open + - returns: The window that is being shown on screen + */ + @available(macOS 10.10, *) + @discardableResult + public func authorizeInNewWindow(at url: URL) throws -> NSWindowController { + let controller = try presentableAuthorizeViewController(at: url) + let wc = windowController(forViewController: controller, with: oauth2.authConfig) + + wc.window?.center() + wc.showWindow(nil) + + return wc + } + + /** + Instantiates and configures an `OAuth2WebViewController`, ready to be used in a window. + + - parameter at: The authorize URL to open + - returns: A web view controller that you can present to the user for login + */ + @available(macOS 10.10, *) + func presentableAuthorizeViewController(at url: URL) throws -> OAuth2WebViewController { + let controller = OAuth2WebViewController() + controller.startURL = url + controller.interceptURLString = oauth2.redirect! + controller.onIntercept = { url in + do { + try self.oauth2.handleRedirectURL(url) + return true + } + catch let error { + self.oauth2.logger?.warn("OAuth2", msg: "Cannot intercept redirect URL: \(error)") + } + return false + } + controller.onWillCancel = { + self.oauth2.didFail(with: nil) + } + return controller + } + + /** + Prepares a window controller with the given web view controller as content. + + - parameter forViewController: The web view controller to use as content + - parameter with: The auth config to use + - returns: A window controller, ready to be presented + */ + @available(macOS 10.10, *) + func windowController(forViewController controller: OAuth2WebViewController, with config: OAuth2AuthConfig) -> NSWindowController { + let rect = NSMakeRect(0, 0, OAuth2WebViewController.webViewWindowWidth, OAuth2WebViewController.webViewWindowHeight) + let window = NSWindow(contentRect: rect, styleMask: [.titled, .closable, .resizable, .fullSizeContentView], backing: .buffered, defer: false) + window.backgroundColor = NSColor.white + window.isMovableByWindowBackground = true + window.titlebarAppearsTransparent = true + window.titleVisibility = .hidden + window.animationBehavior = .alertPanel + if let title = config.ui.title { + window.title = title + } + + let windowController = NSWindowController(window: window) + windowController.contentViewController = controller + + return windowController + } +} + +#endif diff --git a/Sources/OSX/OAuth2WebViewController.swift b/Sources/macOS/OAuth2WebViewController.swift similarity index 62% rename from Sources/OSX/OAuth2WebViewController.swift rename to Sources/macOS/OAuth2WebViewController.swift index a613956d..f486fe15 100644 --- a/Sources/OSX/OAuth2WebViewController.swift +++ b/Sources/macOS/OAuth2WebViewController.swift @@ -3,7 +3,7 @@ // OAuth2 // // Created by Guilherme Rambo on 18/01/16. -// Copyright 2014 Pascal Pfiffner +// Copyright 2016 Pascal Pfiffner // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,15 +17,19 @@ // See the License for the specific language governing permissions and // limitations under the License. // +#if os(macOS) import Cocoa import WebKit +#if !NO_MODULE_IMPORT +import Base +#endif /** A view controller that allows you to display the login/authorization screen. */ -@available(OSX 10.10, *) +@available(macOS 10.10, *) public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NSWindowDelegate { init() { @@ -33,15 +37,15 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS } /// Handle to the OAuth2 instance in play, only used for debug logging at this time. - var oauth: OAuth2? + var oauth: OAuth2Base? /// Configure the view to be shown as sheet, false by default; must be present before the view gets loaded. var willBecomeSheet = false /// The URL to load on first show. - public var startURL: NSURL? { + public var startURL: URL? { didSet(oldURL) { - if nil != startURL && nil == oldURL && viewLoaded { + if nil != startURL && nil == oldURL && isViewLoaded { loadURL(startURL!) } } @@ -51,8 +55,8 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS var interceptURLString: String? { didSet(oldURL) { if nil != interceptURLString { - if let url = NSURL(string: interceptURLString!) { - interceptComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: true) + if let url = URL(string: interceptURLString!) { + interceptComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) } else { oauth?.logger?.warn("OAuth2", msg: "Failed to parse URL \(interceptURLString), discarding") @@ -64,14 +68,14 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS } } } - var interceptComponents: NSURLComponents? + var interceptComponents: URLComponents? /// Closure called when the web view gets asked to load the redirect URL, specified in `interceptURLString`. Return a Bool indicating /// that you've intercepted the URL. - var onIntercept: ((url: NSURL) -> Bool)? + var onIntercept: ((URL) -> Bool)? /// Called when the web view is about to be dismissed manually. - var onWillCancel: (Void -> Void)? + var onWillCancel: ((Void) -> Void)? /// Our web view; implicitly unwrapped so do not attempt to use it unless isViewLoaded() returns true. var webView: WKWebView! @@ -82,14 +86,14 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS view.translatesAutoresizingMaskIntoConstraints = false progressIndicator = NSProgressIndicator(frame: NSZeroRect) - progressIndicator.style = .SpinningStyle - progressIndicator.displayedWhenStopped = false + progressIndicator.style = .spinningStyle + progressIndicator.isDisplayedWhenStopped = false progressIndicator.sizeToFit() progressIndicator.translatesAutoresizingMaskIntoConstraints = false 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.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)) return view } @@ -101,11 +105,11 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS // MARK: - View Handling - internal static let WebViewWindowWidth = CGFloat(600.0) - internal static let WebViewWindowHeight = CGFloat(500.0) + internal static let webViewWindowWidth = CGFloat(600.0) + internal static let webViewWindowHeight = CGFloat(500.0) override public func loadView() { - view = NSView(frame: NSMakeRect(0, 0, OAuth2WebViewController.WebViewWindowWidth, OAuth2WebViewController.WebViewWindowHeight)) + view = NSView(frame: NSMakeRect(0, 0, OAuth2WebViewController.webViewWindowWidth, OAuth2WebViewController.webViewWindowHeight)) view.translatesAutoresizingMaskIntoConstraints = false webView = WKWebView(frame: view.bounds, configuration: WKWebViewConfiguration()) @@ -114,10 +118,10 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS webView.alphaValue = 0.0 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: 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.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: 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)) // add a dismiss button if willBecomeSheet { @@ -127,8 +131,8 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS button.target = self button.action = #selector(OAuth2WebViewController.cancel(_:)) view.addSubview(button) - view.addConstraint(NSLayoutConstraint(item: button, attribute: .Trailing, relatedBy: .Equal, toItem: view, attribute: .Trailing, multiplier: 1.0, constant: -10.0)) - view.addConstraint(NSLayoutConstraint(item: button, attribute: .Bottom, relatedBy: .Equal, toItem: view, attribute: .Bottom, multiplier: 1.0, constant: -10.0)) + view.addConstraint(NSLayoutConstraint(item: button, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1.0, constant: -10.0)) + view.addConstraint(NSLayoutConstraint(item: button, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.0, constant: -10.0)) } showLoadingIndicator() @@ -147,7 +151,7 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS } } - public override func viewDidAppear() { + override public func viewDidAppear() { super.viewDidAppear() view.window?.delegate = self @@ -157,10 +161,10 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS let loadingContainerView = loadingView view.addSubview(loadingContainerView) - view.addConstraint(NSLayoutConstraint(item: loadingContainerView, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1.0, constant: 0.0)) - view.addConstraint(NSLayoutConstraint(item: loadingContainerView, attribute: .Bottom, relatedBy: .Equal, toItem: view, attribute: .Bottom, multiplier: 1.0, constant: 0.0)) - view.addConstraint(NSLayoutConstraint(item: loadingContainerView, attribute: .Left, relatedBy: .Equal, toItem: view, attribute: .Left, multiplier: 1.0, constant: 0.0)) - view.addConstraint(NSLayoutConstraint(item: loadingContainerView, attribute: .Right, relatedBy: .Equal, toItem: view, attribute: .Right, multiplier: 1.0, constant: 0.0)) + view.addConstraint(NSLayoutConstraint(item: loadingContainerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 0.0)) + view.addConstraint(NSLayoutConstraint(item: loadingContainerView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.0, constant: 0.0)) + view.addConstraint(NSLayoutConstraint(item: loadingContainerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 0.0)) + view.addConstraint(NSLayoutConstraint(item: loadingContainerView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: 0.0)) progressIndicator.startAnimation(nil) } @@ -172,7 +176,7 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS progressIndicator.superview?.removeFromSuperview() } - func showErrorMessage(message: String, animated: Bool) { + func showErrorMessage(_ message: String, animated: Bool) { hideLoadingIndicator() webView.animator().alphaValue = 1.0 webView.loadHTMLString("

\(message)

", baseURL: nil) @@ -181,15 +185,15 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS // MARK: - Actions - public func loadURL(url: NSURL) { - webView.loadRequest(NSURLRequest(URL: url)) + public func loadURL(_ url: URL) { + webView.load(URLRequest(url: url)) } - func goBack(sender: AnyObject?) { + func goBack(_ sender: AnyObject?) { webView.goBack() } - func cancel(sender: AnyObject?) { + func cancel(_ sender: AnyObject?) { webView.stopLoading() onWillCancel?() } @@ -197,44 +201,44 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS // MARK: - Web View Delegate - public func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) { + @nonobjc + public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) { let request = navigationAction.request - if nil == onIntercept { - decisionHandler(.Allow) + guard let onIntercept = onIntercept else { + decisionHandler(.allow) return } // we compare the scheme and host first, then check the path (if there is any). Not sure if a simple string comparison // would work as there may be URL parameters attached - if let url = request.URL where url.scheme == interceptComponents?.scheme && url.host == interceptComponents?.host { - let haveComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: true) - if let hp = haveComponents?.path, ip = interceptComponents?.path where hp == ip || ("/" == hp + ip) { - if onIntercept!(url: url) { - decisionHandler(.Cancel) + if let url = request.url, url.scheme == interceptComponents?.scheme && url.host == interceptComponents?.host { + let haveComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) + if let hp = haveComponents?.path, let ip = interceptComponents?.path, hp == ip || ("/" == hp + ip) { + if onIntercept(url) { + decisionHandler(.cancel) } else { - decisionHandler(.Allow) + decisionHandler(.allow) } } } - decisionHandler(.Allow) + decisionHandler(.allow) } - public func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) { - if let scheme = interceptComponents?.scheme where "urn" == scheme { - if let path = interceptComponents?.path where path.hasPrefix("ietf:wg:oauth:2.0:oob") { - if let title = webView.title where title.hasPrefix("Success ") { + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + if let scheme = interceptComponents?.scheme, "urn" == scheme { + if let path = interceptComponents?.path, path.hasPrefix("ietf:wg:oauth:2.0:oob") { + if let title = webView.title, title.hasPrefix("Success ") { oauth?.logger?.debug("OAuth2", msg: "Creating redirect URL from document.title") - let qry = title.stringByReplacingOccurrencesOfString("Success ", withString: "") - if let url = NSURL(string: "http://localhost/?\(qry)") { - onIntercept?(url: url) + let qry = title.replacingOccurrences(of: "Success ", with: "") + if let url = URL(string: "http://localhost/?\(qry)") { + _ = onIntercept?(url) return } - else { - oauth?.logger?.warn("OAuth2", msg: "Failed to create a URL with query parts \"\(qry)\"") - } + + oauth?.logger?.warn("OAuth2", msg: "Failed to create a URL with query parts \"\(qry)\"") } } } @@ -243,8 +247,8 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS hideLoadingIndicator() } - public func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) { - if NSURLErrorDomain == error.domain && NSURLErrorCancelled == error.code { + public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + if NSURLErrorDomain == error._domain && NSURLErrorCancelled == error._code { return } // do we still need to intercept "WebKitErrorDomain" error 102? @@ -255,9 +259,11 @@ public class OAuth2WebViewController: NSViewController, WKNavigationDelegate, NS // MARK: - Window Delegate - public func windowShouldClose(sender: AnyObject) -> Bool { + @nonobjc + public func windowShouldClose(_ sender: AnyObject) -> Bool { onWillCancel?() return false } } +#endif diff --git a/Sources/tvOS/OAuth2+tvOS.swift b/Sources/tvOS/OAuth2Authorizer+tvOS.swift similarity index 58% rename from Sources/tvOS/OAuth2+tvOS.swift rename to Sources/tvOS/OAuth2Authorizer+tvOS.swift index 9ac5052b..e27a4dc2 100644 --- a/Sources/tvOS/OAuth2+tvOS.swift +++ b/Sources/tvOS/OAuth2Authorizer+tvOS.swift @@ -17,18 +17,34 @@ // See the License for the specific language governing permissions and // limitations under the License. // +#if os(tvOS) +import Foundation +#if !NO_MODULE_IMPORT +import Base +#endif -extension OAuth2 { + +public final class OAuth2Authorizer: OAuth2AuthorizerUI { + + /// The OAuth2 instance this authorizer belongs to. + public unowned let oauth2: OAuth2Base + + + init(oauth2: OAuth2) { + self.oauth2 = oauth2 + } + // no webview or webbrowser available on tvOS - public final func openAuthorizeURLInBrowser(params: OAuth2StringDict? = nil) throws { - throw OAuth2Error.Generic("Not implemented") + public func openAuthorizeURLInBrowser(_ url: URL) throws { + throw OAuth2Error.generic("Not implemented") } - public func authorizeEmbeddedWith(config: OAuth2AuthConfig, params: OAuth2StringDict? = nil) throws { - throw OAuth2Error.Generic("Not implemented") + public func authorizeEmbedded(with config: OAuth2AuthConfig, at url: URL) throws { + throw OAuth2Error.generic("Not implemented") } } +#endif diff --git a/SwiftKeychain b/SwiftKeychain index 2a55b6a7..3734015a 160000 --- a/SwiftKeychain +++ b/SwiftKeychain @@ -1 +1 @@ -Subproject commit 2a55b6a731bc0a086b68bdeb977389160465037f +Subproject commit 3734015afd390c08e94789cb7e116d83f9e0247c diff --git a/Tests/OAuth2AuthRequest_tests.swift b/Tests/BaseTests/OAuth2AuthRequestTests.swift similarity index 52% rename from Tests/OAuth2AuthRequest_tests.swift rename to Tests/BaseTests/OAuth2AuthRequestTests.swift index 2be6cb38..ec46b7af 100644 --- a/Tests/OAuth2AuthRequest_tests.swift +++ b/Tests/BaseTests/OAuth2AuthRequestTests.swift @@ -1,5 +1,5 @@ // -// OAuth2AuthRequest_tests.swift +// OAuth2AuthRequestTests.swift // OAuth2 // // Created by Pascal Pfiffner on 18/03/16. @@ -19,61 +19,82 @@ // import XCTest + +#if !NO_MODULE_IMPORT +@testable +import Base +@testable +import Flows +#else @testable import OAuth2 +#endif -class OAuth2AuthRequest_Tests: XCTestCase { +class OAuth2AuthRequestTests: XCTestCase { func testMethod() { - let req1 = OAuth2AuthRequest(url: NSURL()) + let url = URL(string: "http://localhost")! + let req1 = OAuth2AuthRequest(url: url) XCTAssertTrue(req1.method == .POST) - let req2 = OAuth2AuthRequest(url: NSURL(), method: .POST) + let req2 = OAuth2AuthRequest(url: url, method: .POST) XCTAssertTrue(req2.method == .POST) - let req3 = OAuth2AuthRequest(url: NSURL(), method: .GET) + let req3 = OAuth2AuthRequest(url: url, method: .GET) XCTAssertTrue(req3.method == .GET) } func testContentType() { - let req = OAuth2AuthRequest(url: NSURL()) - XCTAssertTrue(req.contentType == .WWWForm) + let url = URL(string: "http://localhost")! + let req = OAuth2AuthRequest(url: url) + XCTAssertTrue(req.contentType == .wwwForm) XCTAssertEqual("application/x-www-form-urlencoded; charset=utf-8", req.contentType.rawValue) - req.contentType = .JSON - XCTAssertTrue(req.contentType == .JSON) + req.contentType = .json + XCTAssertTrue(req.contentType == .json) XCTAssertEqual("application/json", req.contentType.rawValue) } + func testHeaders() { + let url = URL(string: "http://localhost")! + let req = OAuth2AuthRequest(url: url) + XCTAssertTrue(0 == req.params.count) + XCTAssertNil(req.headers) + + req.set(header: "Authorize", to: "Basic abc==") + XCTAssertEqual(1, req.headers?.count) + } + func testParams() { - let req = OAuth2AuthRequest(url: NSURL()) + let url = URL(string: "http://localhost")! + let req = OAuth2AuthRequest(url: url) XCTAssertTrue(0 == req.params.count) req.params["a"] = "A" XCTAssertTrue(1 == req.params.count) - req.addParams(params: ["a": "AA", "b": "B"]) + req.add(params: ["a": "AA", "b": "B"]) XCTAssertTrue(2 == req.params.count) XCTAssertEqual("AA", req.params["a"]) req.params["c"] = "A complicated/surprising name & character=fun" - req.params.removeValueForKey("b") + req.params.removeValue(forKey: "b") XCTAssertTrue(2 == req.params.count) let str = req.params.percentEncodedQueryString() XCTAssertEqual("a=AA&c=A+complicated%2Fsurprising+name+%26+character%3Dfun", str) } func testURLComponents() { - let reqNoTLS = OAuth2AuthRequest(url: NSURL(string: "http://not.tls.com")!) + let reqNoTLS = OAuth2AuthRequest(url: URL(string: "http://not.tls.com")!) do { - try reqNoTLS.asURLComponents() + _ = try reqNoTLS.asURLComponents() XCTAssertTrue(false, "Must no longer be here, must throw because we're not using TLS") } - catch OAuth2Error.NotUsingTLS { + catch OAuth2Error.notUsingTLS { } catch let error { - XCTAssertTrue(false, "Must throw “.NotUsingTLS” but threw \(error)") + XCTAssertTrue(false, "Must throw “.notUsingTLS” but threw \(error)") } - let reqP = OAuth2AuthRequest(url: NSURL(string: "https://auth.io")!) + let reqP = OAuth2AuthRequest(url: URL(string: "https://auth.io")!) reqP.params["a"] = "A" do { let comp = try reqP.asURLComponents() @@ -85,7 +106,7 @@ class OAuth2AuthRequest_Tests: XCTestCase { XCTAssertTrue(false, "Must not throw but threw \(error)") } - let reqG = OAuth2AuthRequest(url: NSURL(string: "https://auth.io")!, method: .GET) + let reqG = OAuth2AuthRequest(url: URL(string: "https://auth.io")!, method: .GET) reqG.params["a"] = "A" do { let comp = try reqG.asURLComponents() @@ -102,22 +123,38 @@ class OAuth2AuthRequest_Tests: XCTestCase { func testRequests() { let settings = ["client_id": "id", "client_secret": "secret"] let oauth = OAuth2(settings: settings) - let reqH = OAuth2AuthRequest(url: NSURL(string: "https://auth.io")!) + let reqH = OAuth2AuthRequest(url: URL(string: "https://auth.io")!) + do { + let request = try reqH.asURLRequest(for: oauth) + XCTAssertEqual("Basic aWQ6c2VjcmV0", request.value(forHTTPHeaderField: "Authorization")) + XCTAssertNil(request.httpBody) // because no params are left + } + catch let error { + XCTAssertTrue(false, "Must not throw but threw \(error)") + } + + // test header override + reqH.set(header: "Authorization", to: "Basic def==") + reqH.set(header: "Accept", to: "text/plain, */*") do { - let request = try reqH.asURLRequestFor(oauth) - XCTAssertEqual("Basic aWQ6c2VjcmV0", request.valueForHTTPHeaderField("Authorization")) - XCTAssertNil(request.HTTPBody) // because no params are left + let request = try reqH.asURLRequest(for: oauth) + XCTAssertEqual("Basic def==", request.value(forHTTPHeaderField: "Authorization")) + XCTAssertEqual("text/plain, */*", request.value(forHTTPHeaderField: "Accept")) + XCTAssertNil(request.httpBody) // because no params are left } catch let error { XCTAssertTrue(false, "Must not throw but threw \(error)") } + // test no Auth header oauth.authConfig.secretInBody = true - let reqB = OAuth2AuthRequest(url: NSURL(string: "https://auth.io")!) + let reqB = OAuth2AuthRequest(url: URL(string: "https://auth.io")!) do { - let request = try reqB.asURLRequestFor(oauth) - XCTAssertEqual("client_id=id&client_secret=secret", NSString(data: request.HTTPBody!, encoding: NSUTF8StringEncoding)) - XCTAssertNil(request.valueForHTTPHeaderField("Authorization")) + let request = try reqB.asURLRequest(for: oauth) + let response = String(data: request.httpBody!, encoding: String.Encoding.utf8) ?? "" + XCTAssertTrue(response.contains("client_id=id")) + XCTAssertTrue(response.contains("client_secret=secret")) + XCTAssertNil(request.value(forHTTPHeaderField: "Authorization")) } catch let error { XCTAssertTrue(false, "Must not throw but threw \(error)") diff --git a/Tests/OAuth2_tests.swift b/Tests/BaseTests/OAuth2Tests.swift similarity index 66% rename from Tests/OAuth2_tests.swift rename to Tests/BaseTests/OAuth2Tests.swift index 016d667b..3af9b000 100644 --- a/Tests/OAuth2_tests.swift +++ b/Tests/BaseTests/OAuth2Tests.swift @@ -1,5 +1,5 @@ // -// OAuth2_Tests.swift +// OAuth2Tests.swift // OAuth2 Tests // // Created by Pascal Pfiffner on 6/6/14. @@ -18,13 +18,17 @@ // limitations under the License. // -#if os(OSX) -import Cocoa -#endif import XCTest +#if !NO_MODULE_IMPORT +@testable +import Base +@testable +import Flows +#else @testable import OAuth2 +#endif class OAuth2Tests: XCTestCase { @@ -46,7 +50,7 @@ class OAuth2Tests: XCTestCase { XCTAssertEqual(oauth.clientId, "def", "Must init `client_id`") oauth = genericOAuth2() - XCTAssertEqual(oauth.authURL, NSURL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") + XCTAssertEqual(oauth.authURL, URL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") XCTAssertEqual(oauth.scope!, "login", "Must init `scope`") XCTAssertTrue(oauth.verbose, "Must init `verbose`") XCTAssertFalse(oauth.useKeychain, "Must not use keychain") @@ -55,13 +59,13 @@ class OAuth2Tests: XCTestCase { func testAuthorizeURL() { let oa = genericOAuth2() oa.verbose = false - let auth = try! oa.authorizeURLWithRedirect("oauth2app://callback", scope: "launch", params: ["extra": "param"]) + let auth = try! oa.authorizeURL(withRedirect: "oauth2app://callback", scope: "launch", params: ["extra": "param"]) - let comp = NSURLComponents(URL: auth, resolvingAgainstBaseURL: true)! + let comp = URLComponents(url: auth, resolvingAgainstBaseURL: true)! XCTAssertEqual("https", comp.scheme!, "Need correct scheme") XCTAssertEqual("auth.ful.io", comp.host!, "Need correct host") - let params = OAuth2.paramsFromQuery(comp.percentEncodedQuery!) + let params = OAuth2.params(fromQuery: comp.percentEncodedQuery!) XCTAssertEqual(params["redirect_uri"]!, "oauth2app://callback", "Expecting correct `redirect_uri` in query") XCTAssertEqual(params["scope"]!, "launch", "Expecting `scope` in query") XCTAssertNotNil(params["state"], "Expecting `state` in query") @@ -73,19 +77,19 @@ class OAuth2Tests: XCTestCase { let oa = genericOAuth2() oa.verbose = false oa.clientConfig.refreshToken = "abc" - let req = try! oa.tokenRequestForTokenRefresh().asURLRequestFor(oa) - let auth = req.URL! + let req = try! oa.tokenRequestForTokenRefresh().asURLRequest(for: oa) + let auth = req.url! - let comp = NSURLComponents(URL: auth, resolvingAgainstBaseURL: true)! + let comp = URLComponents(url: auth, resolvingAgainstBaseURL: true)! XCTAssertEqual("https", comp.scheme!, "Need correct scheme") XCTAssertEqual("token.ful.io", comp.host!, "Need correct host") - let params = OAuth2.paramsFromQuery(comp.percentEncodedQuery ?? "") + 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() { + func testDeprecatedAuthorizeCall() { let oa = genericOAuth2() oa.verbose = false XCTAssertFalse(oa.authConfig.authorizeEmbedded) @@ -94,7 +98,7 @@ class OAuth2Tests: XCTestCase { } oa.onFailure = { error in XCTAssertNotNil(error) - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.NoRedirectURL) + XCTAssertEqual(error, OAuth2Error.noRedirectURL) } oa.authorize() XCTAssertFalse(oa.authConfig.authorizeEmbedded) @@ -103,33 +107,53 @@ class OAuth2Tests: XCTestCase { oa.redirect = "myapp://oauth" oa.onFailure = { error in XCTAssertNotNil(error) - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.InvalidAuthorizationContext) + XCTAssertEqual(error, OAuth2Error.invalidAuthorizationContext) + } + oa.afterAuthorizeOrFail = { params, error in + XCTAssertNil(params) + XCTAssertNotNil(error) + XCTAssertEqual(error, OAuth2Error.invalidAuthorizationContext) + } + oa.authorizeEmbedded(from: NSString()) + XCTAssertTrue(oa.authConfig.authorizeEmbedded) + } + + func testAuthorizeCall() { + let oa = genericOAuth2() + oa.verbose = false + XCTAssertFalse(oa.authConfig.authorizeEmbedded) + oa.authorize() { params, error in + XCTAssertNil(params, "Should not have auth parameters") + XCTAssertNotNil(error) + XCTAssertEqual(error, OAuth2Error.noRedirectURL) } - oa.afterAuthorizeOrFailure = { wasFailure, error in - XCTAssertTrue(wasFailure) + XCTAssertFalse(oa.authConfig.authorizeEmbedded) + + // embedded + oa.redirect = "myapp://oauth" + oa.authorizeEmbedded(from: NSString()) { parameters, error in XCTAssertNotNil(error) - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.InvalidAuthorizationContext) + XCTAssertEqual(error, OAuth2Error.invalidAuthorizationContext) } - oa.authorizeEmbeddedFrom("A string") XCTAssertTrue(oa.authConfig.authorizeEmbedded) } func testQueryParamParsing() { - let params1 = OAuth2.paramsFromQuery("access_token=xxx&expires=2015-00-00&more=stuff") + let params1 = OAuth2.params(fromQuery: "access_token=xxx&expires=2015-00-00&more=stuff") XCTAssert(3 == params1.count, "Expecting 3 URL params") XCTAssertEqual(params1["access_token"]!, "xxx") XCTAssertEqual(params1["expires"]!, "2015-00-00") XCTAssertEqual(params1["more"]!, "stuff") - let params2 = OAuth2.paramsFromQuery("access_token=x%26x&expires=2015-00-00&more=spacey%20stuff") + let params2 = OAuth2.params(fromQuery: "access_token=x%26x&expires=2015-00-00&more=spacey%20stuff") XCTAssert(3 == params1.count, "Expecting 3 URL params") XCTAssertEqual(params2["access_token"]!, "x&x") XCTAssertEqual(params2["expires"]!, "2015-00-00") XCTAssertEqual(params2["more"]!, "spacey stuff") - let params3 = OAuth2.paramsFromQuery("access_token=xxx%3D%3D&expires=2015-00-00&more=spacey+stuff+with+a+%2B") + let params3 = OAuth2.params(fromQuery: "access_token=xxx%3D%3D&expires=2015-00-00&more=spacey+stuff+with+a+%2B") XCTAssert(3 == params1.count, "Expecting 3 URL params") XCTAssertEqual(params3["access_token"]!, "xxx==") @@ -138,20 +162,20 @@ class OAuth2Tests: XCTestCase { } func testQueryParamConversion() { - let qry = OAuth2AuthRequestParams.formEncodedQueryStringFor(["a": "AA", "b": "BB", "x": "yz"]) + let qry = OAuth2RequestParams.formEncodedQueryStringFor(["a": "AA", "b": "BB", "x": "yz"]) XCTAssertEqual(14, qry.characters.count, "Expecting a 14 character string") - let dict = OAuth2.paramsFromQuery(qry) + let dict = OAuth2.params(fromQuery: qry) XCTAssertEqual(dict["a"]!, "AA", "Must unpack `a`") XCTAssertEqual(dict["b"]!, "BB", "Must unpack `b`") XCTAssertEqual(dict["x"]!, "yz", "Must unpack `x`") } func testQueryParamEncoding() { - let qry = OAuth2AuthRequestParams.formEncodedQueryStringFor(["uri": "https://api.io", "str": "a string: cool!", "num": "3.14159"]) + let qry = OAuth2RequestParams.formEncodedQueryStringFor(["uri": "https://api.io", "str": "a string: cool!", "num": "3.14159"]) XCTAssertEqual(60, qry.characters.count, "Expecting a 60 character string") - let dict = OAuth2.paramsFromQuery(qry) + let dict = OAuth2.params(fromQuery: qry) XCTAssertEqual(dict["uri"]!, "https://api.io", "Must correctly unpack `uri`") XCTAssertEqual(dict["str"]!, "a string: cool!", "Must correctly unpack `str`") XCTAssertEqual(dict["num"]!, "3.14159", "Must correctly unpack `num`") @@ -159,10 +183,10 @@ class OAuth2Tests: XCTestCase { func testSessionConfiguration() { let oauth = OAuth2(settings: [:]) - XCTAssertEqual(0, oauth.session.configuration.HTTPCookieStorage?.cookies?.count ?? 0, "Expecting ephemeral session configuration by default") + XCTAssertEqual(0, oauth.session.configuration.httpCookieStorage?.cookies?.count ?? 0, "Expecting ephemeral session configuration by default") // custom configuration - oauth.sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration() + oauth.sessionConfiguration = URLSessionConfiguration.default oauth.sessionConfiguration?.timeoutIntervalForRequest = 5.0 XCTAssertEqual(5, oauth.session.configuration.timeoutIntervalForRequest) @@ -172,7 +196,7 @@ class OAuth2Tests: XCTestCase { XCTAssertEqual(5, oauth.session.configuration.timeoutIntervalForRequest) } - class SessDelegate: NSObject, NSURLSessionDelegate { + class SessDelegate: NSObject, URLSessionDelegate { } } diff --git a/Tests/DataLoaderTests/OAuth2DataLoaderTests.swift b/Tests/DataLoaderTests/OAuth2DataLoaderTests.swift new file mode 100644 index 00000000..3a6ff3b2 --- /dev/null +++ b/Tests/DataLoaderTests/OAuth2DataLoaderTests.swift @@ -0,0 +1,112 @@ +// +// OAuth2DataLoaderTests.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 9/12/16. +// Copyright © 2016 Pascal Pfiffner. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +#if !NO_MODULE_IMPORT + @testable + import Base + @testable + import Flows +#else + @testable + import OAuth2 +#endif + + +class OAuth2DataLoaderTests: XCTestCase { + + var oauth2: OAuth2PasswordGrant? + + var loader: OAuth2DataLoader? + + var authPerformer: OAuth2MockPerformer? + + var dataPerformer: OAuth2AnyBearerPerformer? + + override func setUp() { + super.setUp() + authPerformer = OAuth2MockPerformer() + authPerformer!.responseJSON = ["access_token": "toktok", "token_type": "bearer"] + oauth2 = OAuth2PasswordGrant(settings: ["client_id": "abc", "authorize_url": "https://oauth.io/authorize", "keychain": false] as OAuth2JSON) + oauth2!.logger = OAuth2DebugLogger(.debug) +// oauth2!.logger = OAuth2DebugLogger(.trace) + oauth2!.username = "p2" + oauth2!.password = "test" + oauth2!.requestPerformer = authPerformer + + dataPerformer = OAuth2AnyBearerPerformer() + loader = OAuth2DataLoader(oauth2: oauth2!) + loader!.requestPerformer = dataPerformer + } + + func testAutoEnqueue() { + XCTAssertNil(oauth2!.accessToken) + let req1 = oauth2!.request(forURL: URL(string: "http://auth.io/data/user")!) + let wait1 = expectation(description: "req1") + loader!.perform(request: req1) { response in + XCTAssertNotNil(self.oauth2!.accessToken) + do { + let json = try response.responseJSON() + XCTAssertNotNil(json["data"]) + } + catch let error { + XCTAssertNil(error) + } + wait1.fulfill() + } + + let req2 = oauth2!.request(forURL: URL(string: "http://auth.io/data/home")!) + let wait2 = expectation(description: "req2") + loader!.perform(request: req2) { response in + XCTAssertNotNil(self.oauth2!.accessToken) + do { + let json = try response.responseJSON() + XCTAssertNotNil(json["data"]) + } + catch let error { + XCTAssertNil(error) + } + wait2.fulfill() + } + waitForExpectations(timeout: 4.0) { error in + XCTAssertNil(error) + } + } +} + + +class OAuth2AnyBearerPerformer: OAuth2RequestPerformer { + + func perform(request: URLRequest, completionHandler callback: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask? { + let authorized = (nil != request.value(forHTTPHeaderField: "Authorization")) + let status = authorized ? 201 : 401 + let http = HTTPURLResponse(url: request.url!, statusCode: status, httpVersion: nil, headerFields: nil)! + if authorized { + let data = try? JSONSerialization.data(withJSONObject: ["data": ["in": "response"]], options: []) + callback(data, http, nil) + } + else { + callback(nil, http, nil) + } + return nil + } +} + diff --git a/Tests/OAuth2ClientCredentials_tests.swift b/Tests/FlowTests/OAuth2ClientCredentialsTests.swift similarity index 79% rename from Tests/OAuth2ClientCredentials_tests.swift rename to Tests/FlowTests/OAuth2ClientCredentialsTests.swift index 2d295315..539dddea 100644 --- a/Tests/OAuth2ClientCredentials_tests.swift +++ b/Tests/FlowTests/OAuth2ClientCredentialsTests.swift @@ -1,5 +1,5 @@ // -// OAuth2ClientCredentials_tests.swift +// OAuth2ClientCredentialsTests.swift // OAuth2 // // Created by Pascal Pfiffner on 5/29/15. @@ -20,8 +20,15 @@ import XCTest +#if !NO_MODULE_IMPORT +@testable +import Base +@testable +import Flows +#else @testable import OAuth2 +#endif class OAuth2ClientCredentialsTests: XCTestCase { @@ -50,20 +57,20 @@ class OAuth2ClientCredentialsTests: XCTestCase { XCTAssertEqual(oauth.clientId, "abc", "Must init `client_id`") XCTAssertEqual(oauth.clientSecret!, "def", "Must init `client_secret`") XCTAssertEqual(oauth.scope!, "login and more", "Must init correct scope") - XCTAssertEqual(oauth.authURL, NSURL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") + XCTAssertEqual(oauth.authURL, URL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") XCTAssertFalse(oauth.useKeychain, "Don't use keychain") } func testTokenRequest() { let oauth = genericOAuth2() - let request = try! oauth.tokenRequest().asURLRequestFor(oauth) - XCTAssertEqual("POST", request.HTTPMethod, "Must be a POST request") + let request = try! oauth.accessTokenRequest().asURLRequest(for: oauth) + XCTAssertEqual("POST", request.httpMethod, "Must be a POST request") let authHeader = request.allHTTPHeaderFields?["Authorization"] XCTAssertNotNil(authHeader, "Must create “Authorization” header") XCTAssertEqual(authHeader!, "Basic YWJjOmRlZg==", "Must correctly Base64 encode header") - let body = NSString(data: request.HTTPBody!, encoding: NSUTF8StringEncoding) + let body = String(data: request.httpBody!, encoding: String.Encoding.utf8) XCTAssertNotNil(body, "Body data must be present") XCTAssertEqual(body!, "grant_type=client_credentials&scope=login+and+more", "Must create correct request body") } @@ -77,10 +84,10 @@ class OAuth2ClientCredentialsTests: XCTestCase { ]) do { - try oauth.tokenRequest() + _ = try oauth.accessTokenRequest() XCTAssertFalse(true, "`tokenRequest()` without client secret must throw .NoClientSecret") } - catch OAuth2Error.NoClientSecret { + catch OAuth2Error.noClientSecret { } catch let err { XCTAssertFalse(true, "`tokenRequest()` without client secret must throw .NoClientSecret, but threw \(err)") @@ -89,10 +96,10 @@ class OAuth2ClientCredentialsTests: XCTestCase { func testTokenRequestNoScope() { let oauth = genericOAuth2NoScope() - let request = try! oauth.tokenRequest().asURLRequestFor(oauth) - XCTAssertEqual("POST", request.HTTPMethod, "Must be a POST request") + let request = try! oauth.accessTokenRequest().asURLRequest(for: oauth) + XCTAssertEqual("POST", request.httpMethod, "Must be a POST request") - let body = NSString(data: request.HTTPBody!, encoding: NSUTF8StringEncoding) + let body = String(data: request.httpBody!, encoding: String.Encoding.utf8) XCTAssertNotNil(body, "Body data must be present") XCTAssertEqual(body!, "grant_type=client_credentials", "Must create correct request body") } @@ -106,10 +113,10 @@ class OAuth2ClientCredentialsTests: XCTestCase { ]) do { - try oauth.tokenRequest() + _ = try oauth.accessTokenRequest() XCTAssertFalse(true, "`tokenRequest()` without device_id must throw .Generic") } - catch OAuth2Error.Generic(let message) { + catch OAuth2Error.generic(let message) { XCTAssertEqual("You must configure this flow with a `device_id` (via settings) or manually assign `deviceId`", message) } catch let err { @@ -118,8 +125,8 @@ class OAuth2ClientCredentialsTests: XCTestCase { oauth.deviceId = "def" do { - let req = try oauth.tokenRequest().asURLRequestFor(oauth) - XCTAssertEqual("Basic YWJjOg==", req.valueForHTTPHeaderField("Authorization")) + let req = try oauth.accessTokenRequest().asURLRequest(for: oauth) + XCTAssertEqual("Basic YWJjOg==", req.value(forHTTPHeaderField: "Authorization")) } catch let err { XCTAssertFalse(true, "`tokenRequest()` should not have thrown but threw \(err)") @@ -135,7 +142,7 @@ class OAuth2ClientCredentialsTests: XCTestCase { ]) do { - try oauth.tokenRequest() + _ = try oauth.accessTokenRequest() } catch let err { XCTAssertFalse(true, "`tokenRequest()` should not have thrown but threw \(err)") diff --git a/Tests/OAuth2CodeGrant_tests.swift b/Tests/FlowTests/OAuth2CodeGrantTests.swift similarity index 64% rename from Tests/OAuth2CodeGrant_tests.swift rename to Tests/FlowTests/OAuth2CodeGrantTests.swift index 1d6d191d..ef8f2ea5 100644 --- a/Tests/OAuth2CodeGrant_tests.swift +++ b/Tests/FlowTests/OAuth2CodeGrantTests.swift @@ -1,5 +1,5 @@ // -// OAuth2CodeGrant.swift +// OAuth2CodeGrantTests.swift // OAuth2 // // Created by Pascal Pfiffner on 6/18/14. @@ -20,8 +20,15 @@ import XCTest +#if !NO_MODULE_IMPORT +@testable +import Base +@testable +import Flows +#else @testable import OAuth2 +#endif class OAuth2CodeGrantTests: XCTestCase { @@ -41,8 +48,8 @@ class OAuth2CodeGrantTests: XCTestCase { XCTAssertFalse(oauth.useKeychain, "No keychain") XCTAssertNil(oauth.scope, "Empty scope") - XCTAssertEqual(oauth.authURL, NSURL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") - XCTAssertEqual(oauth.tokenURL!, NSURL(string: "https://token.ful.io")!, "Must init `token_uri`") + XCTAssertEqual(oauth.authURL, URL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") + XCTAssertEqual(oauth.tokenURL!, URL(string: "https://token.ful.io")!, "Must init `token_uri`") } func testNotTLS() { @@ -56,20 +63,20 @@ class OAuth2CodeGrantTests: XCTestCase { XCTAssertNotNil(oauth.authURL, "Must init `authorize_uri`") do { - try oauth.authorizeURLWithRedirect("oauth2://callback", scope: nil, params: nil) + _ = try oauth.authorizeURL(withRedirect: "oauth2://callback", scope: nil, params: nil) XCTAssertTrue(false, "Should no longer be here") } - catch OAuth2Error.NotUsingTLS { + catch OAuth2Error.notUsingTLS { } catch let error { XCTAssertNil(error, "Should not be catching") } do { - try oauth.tokenRequestWithCode("pp").asURL() + _ = try oauth.accessTokenRequest(with: "pp").asURL() XCTAssertTrue(false, "Should no longer be here") } - catch OAuth2Error.NotUsingTLS { + catch OAuth2Error.notUsingTLS { } catch let error { XCTAssertNil(error, "Should not be catching") @@ -80,9 +87,9 @@ class OAuth2CodeGrantTests: XCTestCase { let oauth = OAuth2CodeGrant(settings: baseSettings) XCTAssertNotNil(oauth.authURL, "Must init `authorize_uri`") - let comp = NSURLComponents(URL: try! oauth.authorizeURLWithRedirect("oauth2://callback", scope: nil, params: nil), resolvingAgainstBaseURL: true)! + 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.paramsFromQuery(comp.percentEncodedQuery!) + let query = OAuth2CodeGrant.params(fromQuery: comp.percentEncodedQuery!) XCTAssertEqual(query["client_id"]!, "abc", "Expecting correct `client_id`") XCTAssertNil(query["client_secret"], "Must not have `client_secret`") XCTAssertEqual(query["response_type"]!, "code", "Expecting correct `response_type`") @@ -96,21 +103,21 @@ class OAuth2CodeGrantTests: XCTestCase { oauth.context.redirectURL = oauth.redirect // parse error - var redirect = NSURL(string: "oauth2://callback?error=invalid_scope")! + var redirect = URL(string: "oauth2://callback?error=invalid_scope")! do { - try oauth.validateRedirectURL(redirect) + _ = try oauth.validateRedirectURL(redirect) XCTAssertTrue(false, "Should not be here") } - catch OAuth2Error.InvalidScope { + catch OAuth2Error.invalidScope { } catch let error { XCTAssertTrue(false, "Must not end up here with \(error)") } // parse custom error - redirect = NSURL(string: "oauth2://callback?error=invalid_scope&error_description=BadScopeDude")! + redirect = URL(string: "oauth2://callback?error=invalid_scope&error_description=BadScopeDude")! do { - try oauth.validateRedirectURL(redirect) + _ = try oauth.validateRedirectURL(redirect) XCTAssertTrue(false, "Should not be here") } catch let error { @@ -118,33 +125,33 @@ class OAuth2CodeGrantTests: XCTestCase { } // parse wrong callback - redirect = NSURL(string: "oauth3://callback?error=invalid_scope")! + redirect = URL(string: "oauth3://callback?error=invalid_scope")! do { - try oauth.validateRedirectURL(redirect) + _ = try oauth.validateRedirectURL(redirect) XCTAssertTrue(false, "Should not be here") } - catch OAuth2Error.InvalidRedirectURL { + catch OAuth2Error.invalidRedirectURL { } catch let error { XCTAssertTrue(false, "Should have caught invalid redirect URL error, but got \(error)") } // parse no state - redirect = NSURL(string: "oauth2://callback?code=C0D3")! + redirect = URL(string: "oauth2://callback?code=C0D3")! do { - try oauth.validateRedirectURL(redirect) + _ = try oauth.validateRedirectURL(redirect) XCTAssertTrue(false, "Should not be here") } - catch OAuth2Error.InvalidState { + catch OAuth2Error.missingState { } catch let error { XCTAssertTrue(false, "Must not end up here with \(error)") } // parse all good - redirect = NSURL(string: "oauth2://callback?code=C0D3&state=\(oauth.context.state)")! + redirect = URL(string: "oauth2://callback?code=C0D3&state=\(oauth.context.state)")! do { - try oauth.validateRedirectURL(redirect) + _ = try oauth.validateRedirectURL(redirect) } catch let error { XCTAssertTrue(false, "Should not throw, but threw \(error)") @@ -153,20 +160,20 @@ class OAuth2CodeGrantTests: XCTestCase { // parse oob with invalid redirect oauth.redirect = "urn:ietf:wg:oauth:2.0:oob" oauth.context.redirectURL = oauth.redirect - redirect = NSURL(string: "oauth2://callback?code=C0D3&state=\(oauth.context.state)")! + redirect = URL(string: "oauth2://callback?code=C0D3&state=\(oauth.context.state)")! do { - try oauth.validateRedirectURL(redirect) + _ = try oauth.validateRedirectURL(redirect) } - catch OAuth2Error.InvalidRedirectURL { + catch OAuth2Error.invalidRedirectURL { } catch let error { XCTAssertTrue(false, "Must not end up here with \(error)") } // oob with valid redirect - redirect = NSURL(string: "http://localhost?code=C0D3&state=\(oauth.context.state)")! + redirect = URL(string: "http://localhost?code=C0D3&state=\(oauth.context.state)")! do { - try oauth.validateRedirectURL(redirect) + _ = try oauth.validateRedirectURL(redirect) } catch let error { XCTAssertTrue(false, "Should not throw, but threw \(error)") @@ -184,10 +191,10 @@ class OAuth2CodeGrantTests: XCTestCase { // no redirect in context - fail do { - try oauth.tokenRequestWithCode("pp") + _ = try oauth.accessTokenRequest(with: "pp") XCTAssertTrue(false, "Should not be here any more") } - catch OAuth2Error.NoRedirectURL { + catch OAuth2Error.noRedirectURL { XCTAssertTrue(true, "Must be here") } catch { @@ -197,12 +204,12 @@ class OAuth2CodeGrantTests: XCTestCase { // with redirect in context - success oauth.context.redirectURL = "oauth2://callback" - let req = try! oauth.tokenRequestWithCode("pp").asURLRequestFor(oauth) - let comp = NSURLComponents(URL: req.URL!, resolvingAgainstBaseURL: true)! + 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 = NSString(data: req.HTTPBody!, encoding: NSUTF8StringEncoding) as? String - let query = OAuth2CodeGrant.paramsFromQuery(body!) + 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`") @@ -217,12 +224,12 @@ class OAuth2CodeGrantTests: XCTestCase { oauth.context.redirectURL = "oauth2://callback" // not in body - let req = try! oauth.tokenRequestWithCode("pp").asURLRequestFor(oauth) - let comp = NSURLComponents(URL: req.URL!, resolvingAgainstBaseURL: true)! + 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 = NSString(data: req.HTTPBody!, encoding: NSUTF8StringEncoding) as? String - let query = OAuth2CodeGrant.paramsFromQuery(body!) + let body = String(data: req.httpBody!, encoding: String.Encoding.utf8) + let query = OAuth2CodeGrant.params(fromQuery: body!) XCTAssertNil(query["client_id"], "No `client_id` in body") XCTAssertNil(query["client_secret"], "Must not have `client_secret`") XCTAssertEqual(query["code"]!, "pp", "Expecting correct `code`") @@ -233,12 +240,12 @@ class OAuth2CodeGrantTests: XCTestCase { // in body oauth.authConfig.secretInBody = true - let req2 = try! oauth.tokenRequestWithCode("pp").asURLRequestFor(oauth) - let comp2 = NSURLComponents(URL: req2.URL!, resolvingAgainstBaseURL: true)! + let req2 = try! oauth.accessTokenRequest(with: "pp").asURLRequest(for: oauth) + let comp2 = URLComponents(url: req2.url!, resolvingAgainstBaseURL: true)! XCTAssertEqual(comp2.host!, "token.ful.io", "Correct host") - let body2 = NSString(data: req2.HTTPBody!, encoding: NSUTF8StringEncoding) as? String - let query2 = OAuth2CodeGrant.paramsFromQuery(body2!) + let body2 = String(data: req2.httpBody!, encoding: String.Encoding.utf8) + let query2 = OAuth2CodeGrant.params(fromQuery: body2!) XCTAssertEqual(query2["client_id"]!, "abc", "Expecting correct `client_id`") XCTAssertEqual(query2["client_secret"]!, "xyz", "Expecting correct `client_secret`") XCTAssertEqual(query2["code"]!, "pp", "Expecting correct `code`") @@ -259,8 +266,8 @@ class OAuth2CodeGrantTests: XCTestCase { oauth.redirect = "oauth2://callback" oauth.context.redirectURL = "oauth2://callback" - let req = try! oauth.tokenRequestWithCode("pp").asURLRequestFor(oauth) - let comp = NSURLComponents(URL: req.URL!, resolvingAgainstBaseURL: true)! + let req = try! oauth.accessTokenRequest(with: "pp").asURLRequest(for: oauth) + let comp = URLComponents(url: req.url!, resolvingAgainstBaseURL: true)! XCTAssertEqual(comp.host!, "auth.ful.io", "Correct host") } @@ -270,21 +277,21 @@ class OAuth2CodeGrantTests: XCTestCase { "client_secret": "xyz", "authorize_uri": "https://auth.ful.io", "keychain": false, - ] + ] as [String: Any] let oauth = OAuth2CodeGrant(settings: settings) var response = [ "access_token": "2YotnFZFEjr1zCsicMWpAA", "expires_in": 3600, "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "foo": "bar & hat" - ] + ] as [String: Any] // must throw when "token_type" is missing do { - let _ = try oauth.parseAccessTokenResponse(response) + _ = try oauth.parseAccessTokenResponse(params: response) XCTAssertTrue(false, "Should not be here any more") } - catch OAuth2Error.NoTokenType { + catch OAuth2Error.noTokenType { } catch let error { XCTAssertNil(error, "Should not throw wrong error") @@ -293,7 +300,7 @@ class OAuth2CodeGrantTests: XCTestCase { // LinkedIn on the other hand must not throw let linkedin = OAuth2CodeGrantLinkedIn(settings: settings) do { - let _ = try linkedin.parseAccessTokenResponse(response) + _ = try linkedin.parseAccessTokenResponse(params: response) } catch let error { XCTAssertNil(error, "Should not throw") @@ -302,7 +309,7 @@ class OAuth2CodeGrantTests: XCTestCase { // Nor the generic no-token-type class let noType = OAuth2CodeGrantNoTokenType(settings: settings) do { - let _ = try noType.parseAccessTokenResponse(response) + _ = try noType.parseAccessTokenResponse(params: response) } catch let error { XCTAssertNil(error, "Should not throw") @@ -311,10 +318,10 @@ class OAuth2CodeGrantTests: XCTestCase { // must throw when "token_type" is not known response["token_type"] = "guardian" do { - let _ = try oauth.parseAccessTokenResponse(response) + _ = try oauth.parseAccessTokenResponse(params: response) XCTAssertTrue(false, "Should not be here any more") } - catch OAuth2Error.UnsupportedTokenType(_) { + catch OAuth2Error.unsupportedTokenType(_) { } catch let error { XCTAssertNil(error, "Should not throw wrong error") @@ -322,7 +329,7 @@ class OAuth2CodeGrantTests: XCTestCase { // the no-token-type class must still ignore it do { - let _ = try noType.parseAccessTokenResponse(response) + _ = try noType.parseAccessTokenResponse(params: response) } catch let error { XCTAssertNil(error, "Should not throw") @@ -331,7 +338,7 @@ class OAuth2CodeGrantTests: XCTestCase { // add "token_type" response["token_type"] = "bearer" do { - let dict = try oauth.parseAccessTokenResponse(response) + let dict = try oauth.parseAccessTokenResponse(params: response) XCTAssertEqual("bar & hat", dict["foo"] as? String) XCTAssertEqual("2YotnFZFEjr1zCsicMWpAA", oauth.accessToken, "Must extract access token") XCTAssertNotNil(oauth.accessTokenExpiry, "Must extract access token expiry date") @@ -348,18 +355,64 @@ class OAuth2CodeGrantTests: XCTestCase { "expires_in": 3600, "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "foo": "bar & hat" - ] + ] as [String : Any] do { - try oauth.parseAccessTokenResponse(response2) + _ = try oauth.parseAccessTokenResponse(params: response2) XCTAssertTrue(false, "Should not be here any more") } - catch OAuth2Error.UnsupportedTokenType { + catch OAuth2Error.unsupportedTokenType { XCTAssertTrue(true, "Throw correct error") } catch { XCTAssertTrue(false, "Should not throw wrong error") } + + let performer = OAuth2MockPerformer() + oauth.requestPerformer = performer + + // test round trip - should fail because of 403 + performer.responseJSON = response + performer.responseStatus = 403 + oauth.context.redirectURL = "https://localhost" + oauth.didAuthorizeOrFail = { json, error in + XCTAssertNil(json) + XCTAssertNotNil(error) + XCTAssertEqual(OAuth2Error.forbidden, error) + } + oauth.exchangeCodeForToken("MNOP") + + // test round trip - should succeed because of good HTTP status + performer.responseStatus = 301 + oauth.didAuthorizeOrFail = { json, error in + XCTAssertNotNil(json) + XCTAssertNil(error) + XCTAssertEqual("tGzv3JOkF0XG5Qx2TlKWIA", json?["refresh_token"] as? String) + } + oauth.exchangeCodeForToken("MNOP") + } +} + + +class OAuth2MockPerformer: OAuth2RequestPerformer { + + var responseJSON: OAuth2JSON? + + var responseStatus = 200 + + func perform(request: URLRequest, completionHandler callback: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask? { + let http = HTTPURLResponse(url: request.url!, statusCode: responseStatus, httpVersion: nil, headerFields: nil)! + do { + guard let json = responseJSON else { + throw OAuth2Error.noDataInResponse + } + let data = try JSONSerialization.data(withJSONObject: json, options: []) + callback(data, http, nil) + } + catch let error { + callback(nil, http, error) + } + return nil } } diff --git a/Tests/OAuth2DynReg_tests.swift b/Tests/FlowTests/OAuth2DynRegTests.swift similarity index 80% rename from Tests/OAuth2DynReg_tests.swift rename to Tests/FlowTests/OAuth2DynRegTests.swift index 6e6fb450..4175c9cb 100644 --- a/Tests/OAuth2DynReg_tests.swift +++ b/Tests/FlowTests/OAuth2DynRegTests.swift @@ -1,5 +1,5 @@ // -// OAuth2DynReg_Tests.swift +// OAuth2DynRegTests.swift // OAuth2 // // Created by Pascal Pfiffner on 12/2/15. @@ -19,13 +19,21 @@ // import XCTest + +#if !NO_MODULE_IMPORT +@testable +import Base +@testable +import Flows +#else @testable import OAuth2 +#endif -class OAuth2DynReg_Tests: XCTestCase { +class OAuth2DynRegTests: XCTestCase { - func genericOAuth2(extra: OAuth2JSON? = nil) -> OAuth2 { + func genericOAuth2(_ extra: OAuth2JSON? = nil) -> OAuth2 { var settings = [ "authorize_uri": "https://auth.ful.io", "token_uri": "https://token.ful.io", @@ -46,10 +54,10 @@ class OAuth2DynReg_Tests: XCTestCase { dynreg.extraHeaders = ["Foo": "Bar & Hat"] do { - let req = try dynreg.registrationRequest(oauth) - XCTAssertEqual("register.ful.io", req.URL?.host) - XCTAssertEqual("POST", req.HTTPMethod) - let dict = try oauth.parseJSON(req.HTTPBody!) + let req = try dynreg.registrationRequest(for: oauth) + XCTAssertEqual("register.ful.io", req.url?.host) + XCTAssertEqual("POST", req.httpMethod) + let dict = try oauth.parseJSON(req.httpBody!) XCTAssertEqual("none", dict["token_endpoint_auth_method"] as? String) XCTAssertEqual("login", dict["scope"] as? String) @@ -64,9 +72,9 @@ class OAuth2DynReg_Tests: XCTestCase { func testNotAttemptingRegistration() { let oauth = genericOAuth2() oauth.registerClientIfNeeded() { json, error in - if let error = error as? OAuth2Error { + if let error = error { switch error { - case .NoRegistrationURL: break + case .noRegistrationURL: break default: XCTAssertTrue(false, "Expecting no-registration-url error") } } @@ -90,9 +98,9 @@ class OAuth2DynReg_Tests: XCTestCase { return OAuth2TestDynReg() } oauth.registerClientIfNeeded() { json, error in - if let error = error as? OAuth2Error { + if let error = error { switch error { - case .TemporarilyUnavailable: break + case .temporarilyUnavailable: break default: XCTAssertTrue(false, "Expecting random `TemporarilyUnavailable` error as implemented in `OAuth2TestDynReg`") } } @@ -105,8 +113,8 @@ class OAuth2DynReg_Tests: XCTestCase { class OAuth2TestDynReg: OAuth2DynReg { - override func registerClient(client: OAuth2, callback: ((json: OAuth2JSON?, error: ErrorType?) -> Void)) { - callback(json: nil, error: OAuth2Error.TemporarilyUnavailable) + override func register(client: OAuth2, callback: @escaping ((OAuth2JSON?, OAuth2Error?) -> Void)) { + callback(nil, OAuth2Error.temporarilyUnavailable) } } diff --git a/Tests/FlowTests/OAuth2ImplicitGrantTests.swift b/Tests/FlowTests/OAuth2ImplicitGrantTests.swift new file mode 100644 index 00000000..153d67b1 --- /dev/null +++ b/Tests/FlowTests/OAuth2ImplicitGrantTests.swift @@ -0,0 +1,270 @@ +// +// OAuth2ImplicitGrantTests.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 2/12/15. +// Copyright 2015 Pascal Pfiffner +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +#if !NO_MODULE_IMPORT +@testable +import Base +@testable +import Flows +#else +@testable +import OAuth2 +#endif + + +class OAuth2ImplicitGrantTests: XCTestCase +{ + func testInit() { + let oauth = OAuth2ImplicitGrant(settings: [ + "client_id": "abc", + "keychain": false, + "authorize_uri": "https://auth.ful.io", + ]) + XCTAssertEqual(oauth.clientId, "abc", "Must init `client_id`") + XCTAssertNil(oauth.scope, "Empty scope") + + XCTAssertEqual(oauth.authURL, URL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") + } + + func testDeprecatedReturnURLHandling() { + let oauth = OAuth2ImplicitGrant(settings: [ + "client_id": "abc", + "authorize_uri": "https://auth.ful.io", + "keychain": false, + ]) + + // Empty redirect URL + oauth.onFailure = { error in + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.invalidRedirectURL("file:///")) + } + oauth.afterAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters) + XCTAssertNotNil(error, "Error message expected") + } + oauth.context._state = "ONSTUH" + oauth.handleRedirectURL(URL(string: "file:///")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // No params in redirect URL + oauth.onFailure = { error in + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.invalidRedirectURL("https://auth.ful.io")) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // standard error + oauth.context._state = "ONSTUH" // because it has been reset + oauth.onFailure = { error in + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.accessDenied) + XCTAssertEqual(error?.description, "The resource owner or authorization server denied the request.") + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#error=access_denied")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // explicit error + oauth.context._state = "ONSTUH" // because it has been reset + oauth.onFailure = { error in + XCTAssertNotNil(error, "Error message expected") + XCTAssertNotEqual(error, OAuth2Error.generic("Not good")) + XCTAssertEqual(error, OAuth2Error.responseError("Not good")) + XCTAssertEqual(error?.description, "Not good") + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#error_description=Not+good")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // no token type + oauth.context._state = "ONSTUH" // because it has been reset + oauth.onFailure = { error in + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.noTokenType) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#access_token=abc&state=\(oauth.context.state)")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // unsupported token type + oauth.context._state = "ONSTUH" // because it has been reset + oauth.onFailure = { error in + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.unsupportedTokenType("Only “bearer” token is supported, but received “helicopter”")) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=helicopter&access_token=abc&state=\(oauth.context.state)")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // Invalid state + oauth.context._state = "ONSTUH" // because it has been reset + oauth.onFailure = { error in + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.invalidState) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=ONSTOH")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // success 1 + oauth.onFailure = { error in + XCTAssertTrue(false, "Should not call this") + } + oauth.afterAuthorizeOrFail = { authParameters, error in + XCTAssertNotNil(authParameters, "Expecting non-nil auth dict") + XCTAssertTrue((authParameters?.count ?? 0) > 2, "Expecting non-empty auth dict") + XCTAssertNil(error, "No error message expected") + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=\(oauth.context.state)&expires_in=3599")!) + XCTAssertNotNil(oauth.accessToken, "Must have an access token") + XCTAssertEqual(oauth.accessToken!, "abc") + XCTAssertNotNil(oauth.accessTokenExpiry) + XCTAssertTrue(oauth.hasUnexpiredAccessToken()) + + // success 2 + oauth.onFailure = { error in + XCTAssertTrue(false, "Should not call this") + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=\(oauth.context.state)")!) + XCTAssertNotNil(oauth.accessToken, "Must have an access token") + XCTAssertEqual(oauth.accessToken!, "abc") + XCTAssertNil(oauth.accessTokenExpiry) + XCTAssertTrue(oauth.hasUnexpiredAccessToken()) + } + + func testReturnURLHandling() { + let oauth = OAuth2ImplicitGrant(settings: [ + "client_id": "abc", + "authorize_uri": "https://auth.ful.io", + "keychain": false, + ]) + + // Empty redirect URL + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.invalidRedirectURL("file:///")) + } + oauth.afterAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + } + oauth.context._state = "ONSTUH" + oauth.handleRedirectURL(URL(string: "file:///")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // No params in redirect URL + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.invalidRedirectURL("https://auth.ful.io")) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // standard error + oauth.context._state = "ONSTUH" // because it has been reset + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.accessDenied) + XCTAssertEqual(error?.description, "The resource owner or authorization server denied the request.") + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#error=access_denied")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // explicit error + oauth.context._state = "ONSTUH" // because it has been reset + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + XCTAssertNotEqual(error, OAuth2Error.generic("Not good")) + XCTAssertEqual(error, OAuth2Error.responseError("Not good")) + XCTAssertEqual(error?.description, "Not good") + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#error_description=Not+good")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // no token type + oauth.context._state = "ONSTUH" // because it has been reset + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.noTokenType) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#access_token=abc&state=\(oauth.context.state)")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // unsupported token type + oauth.context._state = "ONSTUH" // because it has been reset + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.unsupportedTokenType("Only “bearer” token is supported, but received “helicopter”")) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=helicopter&access_token=abc&state=\(oauth.context.state)")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // Missing state + oauth.context._state = "ONSTUH" // because it has been reset + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.missingState) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=bearer&access_token=abc")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // Invalid state + oauth.context._state = "ONSTUH" // because it has been reset + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNil(authParameters, "Nil auth dict expected") + XCTAssertNotNil(error, "Error message expected") + XCTAssertEqual(error, OAuth2Error.invalidState) + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=ONSTOH")!) + XCTAssertNil(oauth.accessToken, "Must not have an access token") + + // success 1 + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNotNil(authParameters, "auth parameters expected") + XCTAssertNil(error, "No error message expected") + } + oauth.afterAuthorizeOrFail = { authParameters, error in + XCTAssertNotNil(authParameters, "auth parameters expected") + XCTAssertNil(error, "No error message expected") + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=\(oauth.context.state)&expires_in=3599")!) + XCTAssertNotNil(oauth.accessToken, "Must have an access token") + XCTAssertEqual(oauth.accessToken!, "abc") + XCTAssertNotNil(oauth.accessTokenExpiry) + XCTAssertTrue(oauth.hasUnexpiredAccessToken()) + + // success 2 + oauth.didAuthorizeOrFail = { authParameters, error in + XCTAssertNotNil(authParameters, "auth parameters expected") + XCTAssertNil(error, "No error message expected") + } + oauth.handleRedirectURL(URL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=\(oauth.context.state)")!) + XCTAssertNotNil(oauth.accessToken, "Must have an access token") + XCTAssertEqual(oauth.accessToken!, "abc") + XCTAssertNil(oauth.accessTokenExpiry) + XCTAssertTrue(oauth.hasUnexpiredAccessToken()) + } +} + diff --git a/Tests/OAuth2PasswordGrant_tests.swift b/Tests/FlowTests/OAuth2PasswordGrantTests.swift similarity index 60% rename from Tests/OAuth2PasswordGrant_tests.swift rename to Tests/FlowTests/OAuth2PasswordGrantTests.swift index e5f86177..f8a3243a 100644 --- a/Tests/OAuth2PasswordGrant_tests.swift +++ b/Tests/FlowTests/OAuth2PasswordGrantTests.swift @@ -1,5 +1,5 @@ // -// OAuth2PasswordGrant_Tests.swift +// OAuth2PasswordGrantTests.swift // OAuth2 // // Created by Tim Sneed on 6/5/15. @@ -17,13 +17,22 @@ // See the License for the specific language governing permissions and // limitations under the License. // + import XCTest +#if !NO_MODULE_IMPORT +@testable +import Base +@testable +import Flows +#else @testable import OAuth2 +#endif -class OAuth2PasswordGrantTests: XCTestCase -{ + +class OAuth2PasswordGrantTests: XCTestCase { + func genericOAuth2Password() -> OAuth2PasswordGrant { return OAuth2PasswordGrant(settings: [ "client_id": "abc", @@ -43,35 +52,38 @@ class OAuth2PasswordGrantTests: XCTestCase XCTAssertEqual(oauth.scope!, "login and more", "Must init correct scope") XCTAssertEqual(oauth.username, "My User", "Must init user") XCTAssertEqual(oauth.password, "Here is my password", "Must init password") - XCTAssertEqual(oauth.authURL, NSURL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") + XCTAssertEqual(oauth.authURL, URL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") XCTAssertFalse(oauth.useKeychain, "Don't use keychain") } func testTokenRequest() { let oauth = genericOAuth2Password() - let request = try! oauth.tokenRequest().asURLRequestFor(oauth) - XCTAssertEqual("POST", request.HTTPMethod, "Must be a POST request") + let request = try! oauth.accessTokenRequest().asURLRequest(for: oauth) + XCTAssertEqual("POST", request.httpMethod, "Must be a POST request") let authHeader = request.allHTTPHeaderFields?["Authorization"] XCTAssertNotNil(authHeader, "Must create “Authorization” header") XCTAssertEqual(authHeader!, "Basic YWJjOmRlZg==", "Must correctly Base64 encode header") - let body = NSString(data: request.HTTPBody!, encoding: NSUTF8StringEncoding) + let body = String(data: request.httpBody!, encoding: String.Encoding.utf8) XCTAssertNotNil(body, "Body data must be present") - XCTAssertEqual(body!, "username=My+User&grant_type=password&scope=login+and+more&password=Here+is+my+password", "Must create correct request body") + XCTAssertTrue(body!.contains("username=My+User"), "Must create correct request body") + XCTAssertTrue(body!.contains("grant_type=password"), "Must create correct request body") + XCTAssertTrue(body!.contains("scope=login+and+more"), "Must create correct request body") + XCTAssertTrue(body!.contains("password=Here+is+my+password"), "Must create correct request body") } func testTokenResponse() { let oauth = genericOAuth2Password() let response = [ - "access_token":"2YotnFZFEjr1zCsicMWpAA", - "token_type":"bearer", - "expires_in":3600, - "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", - "foo":"bar" - ] + "access_token": "2YotnFZFEjr1zCsicMWpAA", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", + "foo": "bar" + ] as [String: Any] do { - let dict = try oauth.parseAccessTokenResponse(response) + let dict = try oauth.parseAccessTokenResponse(params: response) XCTAssertEqual("bar", dict["foo"] as? String) XCTAssertEqual("2YotnFZFEjr1zCsicMWpAA", oauth.accessToken, "Must extract access token") XCTAssertNotNil(oauth.accessTokenExpiry, "Must extract access token expiry date") @@ -90,11 +102,15 @@ class OAuth2PasswordGrantTests: XCTestCase "password":"Here is my password", "verbose": true ]) - let request = try! oauth.tokenRequest(params: ["foo": "bar & hat"]).asURLRequestFor(oauth) + let request = try! oauth.accessTokenRequest(params: ["foo": "bar & hat"]).asURLRequest(for: oauth) - let body = NSString(data: request.HTTPBody!, encoding: NSUTF8StringEncoding) + let body = String(data: request.httpBody!, encoding: String.Encoding.utf8) XCTAssertNotNil(body, "Body data must be present") - XCTAssertEqual(body!, "username=My+User&grant_type=password&client_id=abc&foo=bar+%26+hat&password=Here+is+my+password", "Must create correct request body") + XCTAssertTrue(body!.contains("grant_type=password"), "Must create correct request body") + XCTAssertTrue(body!.contains("username=My+User"), "Must create correct request body") + XCTAssertTrue(body!.contains("password=Here+is+my+password"), "Must create correct request body") + XCTAssertTrue(body!.contains("client_id=abc"), "Must add client_id to request body") + XCTAssertTrue(body!.contains("foo=bar+%26+hat"), "Must create correct request body") } } diff --git a/Tests/OAuth2RefreshToken_tests.swift b/Tests/FlowTests/OAuth2RefreshTokenTests.swift similarity index 75% rename from Tests/OAuth2RefreshToken_tests.swift rename to Tests/FlowTests/OAuth2RefreshTokenTests.swift index 7fa69cc1..e8e7a973 100644 --- a/Tests/OAuth2RefreshToken_tests.swift +++ b/Tests/FlowTests/OAuth2RefreshTokenTests.swift @@ -1,5 +1,5 @@ // -// OAuth2RefreshToken_tests.swift +// OAuth2RefreshTokenTests.swift // OAuth2 // // Created by Pascal Pfiffner on 12/20/15. @@ -18,13 +18,17 @@ // limitations under the License. // -#if os(OSX) -import Cocoa -#endif import XCTest +#if !NO_MODULE_IMPORT +@testable +import Base +@testable +import Flows +#else @testable import OAuth2 +#endif class OAuth2RefreshTokenTests: XCTestCase { @@ -41,10 +45,10 @@ class OAuth2RefreshTokenTests: XCTestCase { func testCannotRefresh() { let oauth = genericOAuth2() do { - let _ = try oauth.tokenRequestForTokenRefresh().asURLRequestFor(oauth) + _ = try oauth.tokenRequestForTokenRefresh().asURLRequest(for: oauth) XCTAssertTrue(false, "Should throw when trying to create refresh token request without refresh token") } - catch OAuth2Error.NoRefreshToken { + catch OAuth2Error.noRefreshToken { } catch { XCTAssertTrue(false, "Should have thrown `NoRefreshToken`") @@ -55,17 +59,17 @@ class OAuth2RefreshTokenTests: XCTestCase { let oauth = genericOAuth2() oauth.clientConfig.refreshToken = "pov" - let req = try? oauth.tokenRequestForTokenRefresh().asURLRequestFor(oauth) + let req = try? oauth.tokenRequestForTokenRefresh().asURLRequest(for: oauth) XCTAssertNotNil(req) - XCTAssertNotNil(req?.URL) - XCTAssertNotNil(req?.HTTPBody) - XCTAssertEqual("https://token.ful.io", req!.URL!.absoluteString) - let comp = NSURLComponents(URL: req!.URL!, resolvingAgainstBaseURL: true) + XCTAssertNotNil(req?.url) + XCTAssertNotNil(req?.httpBody) + XCTAssertEqual("https://token.ful.io", req!.url!.absoluteString) + let comp = URLComponents(url: req!.url!, resolvingAgainstBaseURL: true) let params = comp?.percentEncodedQuery XCTAssertNil(params) - let body = NSString(data: req!.HTTPBody!, encoding: NSUTF8StringEncoding) as? String + let body = String(data: req!.httpBody!, encoding: String.Encoding.utf8) XCTAssertNotNil(body) - let dict = OAuth2.paramsFromQuery(body!) + let dict = OAuth2.params(fromQuery: body!) XCTAssertEqual(dict["client_id"], "abc") XCTAssertEqual(dict["refresh_token"], "pov") XCTAssertEqual(dict["grant_type"], "refresh_token") @@ -78,12 +82,12 @@ class OAuth2RefreshTokenTests: XCTestCase { oauth.clientConfig.refreshToken = "pov" oauth.clientConfig.clientSecret = "uvw" - let req = try? oauth.tokenRequestForTokenRefresh().asURLRequestFor(oauth) + let req = try? oauth.tokenRequestForTokenRefresh().asURLRequest(for: oauth) XCTAssertNotNil(req) - XCTAssertNotNil(req?.HTTPBody) - let body = NSString(data: req!.HTTPBody!, encoding: NSUTF8StringEncoding) as? String + XCTAssertNotNil(req?.httpBody) + let body = String(data: req!.httpBody!, encoding: String.Encoding.utf8) XCTAssertNotNil(body) - let dict = OAuth2.paramsFromQuery(body!) + let dict = OAuth2.params(fromQuery: body!) XCTAssertNil(dict["client_id"]) XCTAssertNil(dict["client_secret"]) let auth = req!.allHTTPHeaderFields?["Authorization"] @@ -97,12 +101,12 @@ class OAuth2RefreshTokenTests: XCTestCase { oauth.clientConfig.clientSecret = "uvw" oauth.authConfig.secretInBody = true - let req = try? oauth.tokenRequestForTokenRefresh(params: ["param": "fool"]).asURLRequestFor(oauth) + let req = try? oauth.tokenRequestForTokenRefresh(params: ["param": "fool"]).asURLRequest(for: oauth) XCTAssertNotNil(req) - XCTAssertNotNil(req?.HTTPBody) - let body = NSString(data: req!.HTTPBody!, encoding: NSUTF8StringEncoding) as? String + XCTAssertNotNil(req?.httpBody) + let body = String(data: req!.httpBody!, encoding: String.Encoding.utf8) XCTAssertNotNil(body) - let dict = OAuth2.paramsFromQuery(body!) + let dict = OAuth2.params(fromQuery: body!) XCTAssertEqual(dict["client_id"], "abc") XCTAssertEqual(dict["client_secret"], "uvw") XCTAssertEqual(dict["param"], "fool") diff --git a/Tests/OAuth2ImplicitGrant_tests.swift b/Tests/OAuth2ImplicitGrant_tests.swift deleted file mode 100644 index f6a3ffbc..00000000 --- a/Tests/OAuth2ImplicitGrant_tests.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// OAuth2ImplicitGrant_tests.swift -// OAuth2 -// -// Created by Pascal Pfiffner on 2/12/15. -// Copyright 2015 Pascal Pfiffner -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -@testable -import OAuth2 - - -class OAuth2ImplicitGrantTests: XCTestCase -{ - func testInit() { - let oauth = OAuth2ImplicitGrant(settings: [ - "client_id": "abc", - "keychain": false, - "authorize_uri": "https://auth.ful.io", - ]) - XCTAssertEqual(oauth.clientId, "abc", "Must init `client_id`") - XCTAssertNil(oauth.scope, "Empty scope") - - XCTAssertEqual(oauth.authURL, NSURL(string: "https://auth.ful.io")!, "Must init `authorize_uri`") - } - - func testReturnURLHandling() { - let oauth = OAuth2ImplicitGrant(settings: [ - "client_id": "abc", - "authorize_uri": "https://auth.ful.io", - "keychain": false, - ]) - - // Empty redirect URL - oauth.onFailure = { error in - XCTAssertNotNil(error, "Error message expected") - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.InvalidRedirectURL("")) - } - oauth.afterAuthorizeOrFailure = { wasFailure, error in - XCTAssertTrue(wasFailure) - XCTAssertNotNil(error, "Error message expected") - } - oauth.context._state = "ONSTUH" - oauth.handleRedirectURL(NSURL(string: "")!) - XCTAssertNil(oauth.accessToken, "Must not have an access token") - - // No params in redirect URL - oauth.onFailure = { error in - XCTAssertNotNil(error, "Error message expected") - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.InvalidRedirectURL("https://auth.ful.io")) - } - oauth.handleRedirectURL(NSURL(string: "https://auth.ful.io")!) - XCTAssertNil(oauth.accessToken, "Must not have an access token") - - // standard error - oauth.context._state = "ONSTUH" // because it has been reset - oauth.onFailure = { error in - XCTAssertNotNil(error, "Error message expected") - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.AccessDenied) - XCTAssertEqual((error as! OAuth2Error).description, "The resource owner or authorization server denied the request.") - } - oauth.handleRedirectURL(NSURL(string: "https://auth.ful.io#error=access_denied")!) - XCTAssertNil(oauth.accessToken, "Must not have an access token") - - // explicit error - oauth.context._state = "ONSTUH" // because it has been reset - oauth.onFailure = { error in - XCTAssertNotNil(error, "Error message expected") - XCTAssertNotEqual((error as! OAuth2Error), OAuth2Error.Generic("Not good")) - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.ResponseError("Not good")) - XCTAssertEqual((error as! OAuth2Error).description, "Not good") - } - oauth.handleRedirectURL(NSURL(string: "https://auth.ful.io#error_description=Not+good")!) - XCTAssertNil(oauth.accessToken, "Must not have an access token") - - // no token type - oauth.context._state = "ONSTUH" // because it has been reset - oauth.onFailure = { error in - XCTAssertNotNil(error, "Error message expected") - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.NoTokenType) - } - oauth.handleRedirectURL(NSURL(string: "https://auth.ful.io#access_token=abc&state=\(oauth.context.state)")!) - XCTAssertNil(oauth.accessToken, "Must not have an access token") - - // unsupported token type - oauth.context._state = "ONSTUH" // because it has been reset - oauth.onFailure = { error in - XCTAssertNotNil(error, "Error message expected") - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.UnsupportedTokenType("Only “bearer” token is supported, but received “helicopter”")) - } - oauth.handleRedirectURL(NSURL(string: "https://auth.ful.io#token_type=helicopter&access_token=abc&state=\(oauth.context.state)")!) - XCTAssertNil(oauth.accessToken, "Must not have an access token") - - // Invalid state - oauth.context._state = "ONSTUH" // because it has been reset - oauth.onFailure = { error in - XCTAssertNotNil(error, "Error message expected") - XCTAssertEqual((error as! OAuth2Error), OAuth2Error.InvalidState) - } - oauth.handleRedirectURL(NSURL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=ONSTOH")!) - XCTAssertNil(oauth.accessToken, "Must not have an access token") - - // success 1 - oauth.onFailure = { error in - XCTAssertTrue(false, "Should not call this") - } - oauth.afterAuthorizeOrFailure = { wasFailure, error in - XCTAssertFalse(wasFailure) - XCTAssertNil(error, "No error message expected") - } - oauth.handleRedirectURL(NSURL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=\(oauth.context.state)&expires_in=3599")!) - XCTAssertNotNil(oauth.accessToken, "Must have an access token") - XCTAssertEqual(oauth.accessToken!, "abc") - XCTAssertNotNil(oauth.accessTokenExpiry) - XCTAssertTrue(oauth.hasUnexpiredAccessToken()) - - // success 2 - oauth.onFailure = { error in - XCTAssertTrue(false, "Should not call this") - } - oauth.handleRedirectURL(NSURL(string: "https://auth.ful.io#token_type=bearer&access_token=abc&state=\(oauth.context.state)")!) - XCTAssertNotNil(oauth.accessToken, "Must have an access token") - XCTAssertEqual(oauth.accessToken!, "abc") - XCTAssertNil(oauth.accessTokenExpiry) - XCTAssertTrue(oauth.hasUnexpiredAccessToken()) - } -} - diff --git a/generate-docs.sh b/generate-docs.sh index cad4fce3..b5d39021 100755 --- a/generate-docs.sh +++ b/generate-docs.sh @@ -7,7 +7,7 @@ jazzy \ -m "OAuth2" \ -a "Pascal Pfiffner" \ -o "docs" \ - --module-version "2.3.0" + --module-version "3.0.0" mkdir docs/assets 2>/dev/null cp assets/* docs/assets/ diff --git a/p2.OAuth2.podspec b/p2.OAuth2.podspec index 0e0efd66..45138aff 100644 --- a/p2.OAuth2.podspec +++ b/p2.OAuth2.podspec @@ -7,10 +7,10 @@ Pod::Spec.new do |s| s.name = "p2.OAuth2" - s.version = "2.3.0" - s.summary = "OAuth2 framework for OS X, iOS and tvOS, written in Swift." + s.version = "3.0.0" + s.summary = "OAuth2 framework for macOS, iOS and tvOS, written in Swift." s.description = <<-DESC - OAuth2 frameworks for OS X, iOS and tvOS written in Swift. + OAuth2 frameworks for macOS, iOS and tvOS written in Swift. A flexible framework supporting standards-compliant _implicit_ and _code_ grant flows. Some websites like Facebook may use slightly differring OAuth2 implementations, for those the @@ -20,20 +20,21 @@ Pod::Spec.new do |s| 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.source = { :git => "https://github.com/p2/OAuth2.git", :tag => "#{s.version}", :submodules => true } s.ios.deployment_target = "8.0" s.osx.deployment_target = "10.10" s.tvos.deployment_target = "9.0" - s.requires_arc = true + s.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS" => "-DNO_MODULE_IMPORT -DNO_KEYCHAIN_IMPORT" } - s.source_files = "Sources/Base/*.swift" + 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/OSX/*.swift" + s.osx.source_files = "Sources/macOS/*.swift" s.tvos.source_files = "Sources/tvOS/*.swift" - s.dependency "SwiftKeychain", "~> 1.0" + #s.dependency "SwiftKeychain", "~> 1.0" s.ios.framework = "SafariServices" end