├── Example ├── Source │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── okta-logo.imageset │ │ │ ├── okta-logo.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.storyboard └── OktaAuthNative Example.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── OktaAuthNative Example.xcscheme ├── Tests ├── Resources │ ├── AuthenticationFailedError │ ├── RECOVERY_CHALLENGE_EMAIL │ ├── SUCCESS_UNLOCK │ ├── OperationNotAllowedError │ ├── SUCCESS │ ├── LOCKED_OUT │ ├── RECOVERY │ ├── RECOVERY_CHALLENGE_SMS │ ├── PASSWORD_EXPIRED │ ├── MFA_ENROLL_ACTIVATE_TOTP │ ├── PASSWORD_RESET │ ├── MFA_ENROLL_ACTIVATE_CALL │ ├── PASSWORD_WARN │ ├── Questions │ ├── MFA_CHALLENGE_TOTP │ ├── MFA_ENROLL_ACTIVATE_SMS │ ├── MFA_CHALLENGE_SMS │ ├── MFA_ENROLL_ACTIVATE_Push │ ├── Unknown_State_And_FactorResult │ ├── MFA_CHALLENGE_WAITING_PUSH │ ├── MFA_ENROLL_PartiallyEnrolled │ ├── MFA_ENROLL_NotEnrolled │ └── MFA_REQUIRED ├── Info.plist ├── Mocks │ ├── OktaURLSessionMock.swift │ ├── OktaAuthHTTPClientMock.swift │ ├── OktaAuthStatus+Mocking.swift │ ├── OktaFactorResultProtocolMock.swift │ ├── OktaAuthStatusResponseHandlerMock.swift │ └── OktaFactor+Mocking.swift ├── DomainObjects │ ├── AuthStateTests.swift │ └── TypesTests.swift ├── Statuses │ ├── OktaAuthStatusSuccessTests.swift │ ├── OktaAuthStatePasswordResetTests.swift │ ├── OktaAuthStatusLockedOutTests.swift │ ├── OktaAuthStatusPasswordExpiredTests.swift │ ├── OktaAuthStatusRecoveryTests.swift │ ├── OktaAuthStatusPasswordWarningTests.swift │ ├── OktaAuthStatusFactorRequiredTests.swift │ └── OktaAuthStatusFactorEnrollActivateTests.swift ├── Utils │ └── TestResponse.swift └── Factors │ ├── OktaFactorTestCase.swift │ ├── OktaFactorOtherTests.swift │ └── OktaFactorTokenTests.swift ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yml │ └── bug-report.yml ├── SECURITY.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── workflows │ └── okta-auth.yml ├── OktaAuthNative.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── OktaAuthNative iOS.xcscheme ├── OktaAuthSdk.xcworkspace ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── Source ├── Resources │ └── PrivacyInfo.xcprivacy ├── OktaHTTPRequestListenerProtocol.swift ├── Info.plist ├── Statuses │ ├── OktaAuthStatusSuccess.swift │ ├── OktaAuthStatusLockedOut.swift │ ├── OktaAuthStatusPasswordExpired.swift │ ├── OktaAuthStatePasswordReset.swift │ ├── OktaAuthStatusPasswordWarning.swift │ ├── OktaAuthStatusUnauthenticated.swift │ ├── OktaAuthStatusFactorRequired.swift │ ├── OktaAuthStatusRecovery.swift │ ├── OktaAuthStatusResponseHandler.swift │ ├── OktaAuthStatusFactorEnroll.swift │ ├── OktaAuthStatusFactorChallenge.swift │ ├── OktaAuthStatusFactorEnrollActivate.swift │ ├── OktaAuthStatusRecoveryChallenge.swift │ └── OktaAuthStatus.swift ├── RestAPI │ └── Utils.swift ├── Factors │ ├── OktaFactorOther.swift │ ├── OktaFactorSms.swift │ ├── OktaFactorCall.swift │ ├── OktaFactorToken.swift │ ├── OktaFactorTotp.swift │ └── OktaFactorQuestion.swift ├── OktaError.swift ├── OktaAuthSdk.swift └── DomainObjects │ └── AuthState.swift ├── Package.swift ├── OktaAuthSdk.podspec ├── .circleci └── config.yml ├── .gitignore └── CONTRIBUTING.md /Example/Source/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Source/Assets.xcassets/okta-logo.imageset/okta-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okta/okta-auth-swift/HEAD/Example/Source/Assets.xcassets/okta-logo.imageset/okta-logo.png -------------------------------------------------------------------------------- /Tests/Resources/AuthenticationFailedError: -------------------------------------------------------------------------------- 1 | {"errorCode":"E0000004","errorSummary":"Authentication failed","errorLink":"E0000004","errorId":"oaep_fwxUiwQjSHsMRpsv4pMg","errorCauses":[]} 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Developer Forum 3 | url: https://devforum.okta.com/ 4 | about: Get help with building your application on the Okta Platform. 5 | blank_issues_enabled: false -------------------------------------------------------------------------------- /Tests/Resources/RECOVERY_CHALLENGE_EMAIL: -------------------------------------------------------------------------------- 1 | { 2 | "status": "RECOVERY_CHALLENGE", 3 | "factorResult": "WAITING", 4 | "relayState": "/myapp/some/deep/link/i/want/to/return/to", 5 | "factorType": "EMAIL", 6 | "recoveryType": "PASSWORD" 7 | } 8 | -------------------------------------------------------------------------------- /Example/OktaAuthNative Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Report a Vulnerability 4 | 5 | At Okta we take the protection of our customers’ data very seriously. If you need to report a vulnerability, please visit https://www.okta.com/vulnerability-reporting-policy/ for more information. 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Problem Analysis (Technical) 2 | 3 | 4 | ### Solution (Technical) 5 | 6 | 7 | ### Affected Components 8 | 9 | 10 | ### Steps to reproduce: 11 | 12 | Actual result: 13 | 14 | Expected result: 15 | 16 | ### Tests 17 | -------------------------------------------------------------------------------- /OktaAuthNative.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/Resources/SUCCESS_UNLOCK: -------------------------------------------------------------------------------- 1 | {"status":"SUCCESS","recoveryType":"UNLOCK","_embedded":{"user":{"id":"00u2rejxahomC7fZP0g7","passwordChanged":"2019-05-23T20:17:50.000Z","profile":{"login":"ildar.abdullin@okta.com","firstName":"Ildar","lastName":"Abdullin","locale":"en","timeZone":"America/Los_Angeles"}}}} 2 | -------------------------------------------------------------------------------- /Tests/Resources/OperationNotAllowedError: -------------------------------------------------------------------------------- 1 | {"errorCode":"E0000079","errorSummary":"This operation is not allowed in the current authentication state.","errorLink":"E0000079","errorId":"oae601OOxRbRImSztKcjduZ2w","errorCauses":[{"errorSummary":"This operation is not allowed in the current authentication state."}]} 2 | -------------------------------------------------------------------------------- /OktaAuthSdk.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OktaAuthNative.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OktaAuthSdk.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/OktaAuthNative Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Source/Assets.xcassets/okta-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "okta-logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Source/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | NSPrivacyTracking 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyCollectedDataTypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Tests/Resources/SUCCESS: -------------------------------------------------------------------------------- 1 | { 2 | "expiresAt":"2019-04-18T15:53:30.000Z", 3 | "status":"SUCCESS", 4 | "sessionToken":"test_session_token", 5 | "_embedded":{ 6 | "user":{ 7 | "id":"test_id", 8 | "passwordChanged":"2019-02-11T14:09:22.000Z", 9 | "profile":{ 10 | "login":"test_user", 11 | "firstName":"test_first_name", 12 | "lastName":"test_last_name", 13 | "locale":"en", 14 | "timeZone":"America/Los_Angeles" 15 | } 16 | } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Tests/Resources/LOCKED_OUT: -------------------------------------------------------------------------------- 1 | { 2 | "status":"LOCKED_OUT", 3 | "_embedded":{ 4 | 5 | }, 6 | "_links":{ 7 | "next":{ 8 | "name":"unlock", 9 | "href":"https://lohika-um.oktapreview.com/api/v1/authn/recovery/unlock", 10 | "hints":{ 11 | "allow":[ 12 | "POST" 13 | ] 14 | } 15 | }, 16 | "cancel":{ 17 | "href":"https://lohika-um.oktapreview.com/api/v1/authn/cancel", 18 | "hints":{ 19 | "allow":[ 20 | "POST" 21 | ] 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "OktaAuthNative", 8 | platforms: [ 9 | .macOS(.v10_14), .iOS(.v10) 10 | ], 11 | products: [ 12 | .library(name: "OktaAuthNative", targets: ["OktaAuthNative"]) 13 | ], 14 | targets: [ 15 | .target(name: "OktaAuthNative", dependencies: [],path: "Source", exclude: ["Info.plist"], resources: [.process("Resources")]), 16 | .testTarget(name: "OktaAuthNative_Tests", dependencies: ["OktaAuthNative"], path: "Tests", exclude: ["AuthenticationClientTests.swift"]) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/okta-auth.yml: -------------------------------------------------------------------------------- 1 | name: Okta Auth Swift 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | UnitTests: 11 | runs-on: macos-latest 12 | env: 13 | DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: iOS 17 | run: xcodebuild -workspace OktaAuthSdk.xcworkspace -scheme "OktaAuthNative iOS" -destination "platform=iOS Simulator,OS=latest,name=iPhone 15 Pro Max" clean test 18 | - name: Swift 19 | run: swift test -v 20 | PackageValidation: 21 | runs-on: macos-latest 22 | env: 23 | DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Cocoapods 27 | run: pod lib lint --allow-warnings 28 | -------------------------------------------------------------------------------- /Source/OktaHTTPRequestListenerProtocol.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | public protocol OktaHTTPRequestListenerProtocol { 16 | func sendRequest(_ request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) 17 | } 18 | -------------------------------------------------------------------------------- /Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | OktaAuth 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/Resources/RECOVERY: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken": "00xdqXOE5qDXX8-PBR1bYv8AESqIEinDy3yul01tyh", 3 | "expiresAt": "2015-11-03T10:15:57.000Z", 4 | "status": "RECOVERY", 5 | "recoveryType": "PASSWORD", 6 | "_embedded": { 7 | "user": { 8 | "id": "00ub0oNGTSWTBKOLGLNR", 9 | "passwordChanged": "2015-09-08T20:14:45.000Z", 10 | "profile": { 11 | "login": "dade.murphy@example.com", 12 | "firstName": "Dade", 13 | "lastName": "Murphy", 14 | "locale": "en_US", 15 | "timeZone": "America/Los_Angeles" 16 | }, 17 | "recovery_question": { 18 | "question": "Who's a major player in the cowboy scene?" 19 | } 20 | } 21 | }, 22 | "_links": { 23 | "next": { 24 | "name": "answer", 25 | "href": "https://yourOktaDomain/api/v1/authn/recovery/answer", 26 | "hints": { 27 | "allow": [ 28 | "POST" 29 | ] 30 | } 31 | }, 32 | "cancel": { 33 | "href": "https://yourOktaDomain/api/v1/authn/cancel", 34 | "hints": { 35 | "allow": [ 36 | "POST" 37 | ] 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /OktaAuthSdk.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'OktaAuthSdk' 3 | s.version = '2.4.6' 4 | s.summary = 'SDK for Okta native authentication.' 5 | s.description = <<-DESC 6 | Integrate your native app with Okta. 7 | DESC 8 | s.platforms = { :ios => "10.0", :osx => "10.14"} 9 | s.homepage = 'https://github.com/okta/okta-auth-swift' 10 | s.license = { :type => 'APACHE2', :file => 'LICENSE' } 11 | s.authors = { "Okta Developers" => "developer@okta.com"} 12 | s.source = { :git => 'https://github.com/okta/okta-auth-swift.git', :tag => s.version.to_s } 13 | 14 | s.ios.deployment_target = '10.0' 15 | s.osx.deployment_target = '10.14' 16 | s.source_files = 'Source/**/*' 17 | s.resource_bundles = { 'OktaAuthSdk' => 'Source/Resources/**/*' } 18 | s.swift_version = '5.0' 19 | s.exclude_files = [ 20 | 'Source/Info.plist' 21 | ] 22 | end 23 | -------------------------------------------------------------------------------- /Tests/Resources/RECOVERY_CHALLENGE_SMS: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken": "00xdqXOE5qDXX8-PBR1bYv8AESqIEinDy3yul01tyh", 3 | "expiresAt": "2015-11-03T10:15:57.000Z", 4 | "status": "RECOVERY_CHALLENGE", 5 | "relayState": "/myapp/some/deep/link/i/want/to/return/to", 6 | "factorType": "SMS", 7 | "recoveryType": "PASSWORD", 8 | "_links": { 9 | "next": { 10 | "name": "verify", 11 | "href": "https://yourOktaDomain/api/v1/authn/recovery/factors/SMS/verify", 12 | "hints": { 13 | "allow": [ 14 | "POST" 15 | ] 16 | } 17 | }, 18 | "cancel": { 19 | "href": "https://yourOktaDomain/api/v1/authn/cancel", 20 | "hints": { 21 | "allow": [ 22 | "POST" 23 | ] 24 | } 25 | }, 26 | "resend": { 27 | "name": "sms", 28 | "href": "https://yourOktaDomain/api/v1/authn/recovery/factors/SMS/resend", 29 | "hints": { 30 | "allow": [ 31 | "POST" 32 | ] 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Example/Source/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import UIKit 14 | 15 | @UIApplicationMain 16 | class AppDelegate: UIResponder, UIApplicationDelegate { 17 | 18 | var window: UIWindow? 19 | 20 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 21 | return true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/Mocks/OktaURLSessionMock.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | class OktaURLSessionMock: URLSession { 16 | var didSendRequest = false 17 | 18 | override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 19 | didSendRequest = true 20 | return URLSessionDataTask() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature for this sample? 3 | labels: [ enhancement ] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Describe the feature request? 9 | description: | 10 | Please leave a helpful description of the feature request here. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: resources 16 | attributes: 17 | label: New or Affected Resource(s) 18 | description: | 19 | Please list the new or affected resources 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: documentation 25 | attributes: 26 | label: Provide a documentation link 27 | description: | 28 | Please provide any links to the documentation that is at 29 | https://developer.okta.com/. This will help us with this 30 | feature request. 31 | 32 | - type: textarea 33 | id: additional 34 | attributes: 35 | label: Additional Information? -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusSuccess.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusSuccess : OktaAuthStatus { 16 | 17 | open var recoveryType: OktaAPISuccessResponse.RecoveryType? { 18 | get { 19 | return model.recoveryType 20 | } 21 | } 22 | 23 | public var sessionToken: String? 24 | 25 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 26 | self.sessionToken = model.sessionToken 27 | try super.init(currentState: currentState, model: model) 28 | statusType = .success 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/Resources/PASSWORD_EXPIRED: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", 3 | "expiresAt": "2015-11-03T10:15:57.000Z", 4 | "status": "PASSWORD_EXPIRED", 5 | "relayState": "/myapp/some/deep/link/i/want/to/return/to", 6 | "_embedded": { 7 | "user": { 8 | "id": "00ub0oNGTSWTBKOLGLNR", 9 | "passwordChanged": "2015-09-08T20:14:45.000Z", 10 | "profile": { 11 | "login": "dade.murphy@example.com", 12 | "firstName": "Dade", 13 | "lastName": "Murphy", 14 | "locale": "en_US", 15 | "timeZone": "America/Los_Angeles" 16 | } 17 | }, 18 | "policy": { 19 | "complexity": { 20 | "minLength": 8, 21 | "minLowerCase": 1, 22 | "minUpperCase": 1, 23 | "minNumber": 1, 24 | "minSymbol": 0 25 | } 26 | } 27 | }, 28 | "_links": { 29 | "next": { 30 | "name": "changePassword", 31 | "href": "https://okta.okta.com/api/v1/authn/credentials/change_password", 32 | "hints": { 33 | "allow": [ 34 | "POST" 35 | ] 36 | } 37 | }, 38 | "cancel": { 39 | "href": "https://okta.okta.com/api/v1/authn/cancel", 40 | "hints": { 41 | "allow": [ 42 | "POST" 43 | ] 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Tests/Resources/MFA_ENROLL_ACTIVATE_TOTP: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"00_id3fUof3vX9Fy1H3qejz7izKh1gou0rOWkwJesi", 3 | "expiresAt":"2019-04-18T15:43:41.000Z", 4 | "status":"MFA_ENROLL_ACTIVATE", 5 | "_embedded":{ 6 | "user":{ 7 | "id":"00ujbyedtoxuuXn9l0h7", 8 | "passwordChanged":"2019-02-11T14:09:22.000Z", 9 | "profile":{ 10 | "login":"ayurok", 11 | "firstName":"ayurok", 12 | "lastName":"ayurok", 13 | "locale":"en", 14 | "timeZone":"America/Los_Angeles" 15 | } 16 | }, 17 | "factor":{ 18 | "id":"opfkdh40kws5XarDb0h7", 19 | "factorType":"token:software:totp", 20 | "provider":"OKTA", 21 | "vendorName":"OKTA", 22 | "_embedded":{ 23 | "activation":{ 24 | "expiresAt":"2019-04-18T15:48:41.000Z", 25 | "_links":{ 26 | "qrcode":{ 27 | "href":"https://test.domain.com.com/api/v1/users/factors/qr/cQYp5xpm", 28 | "type":"image/png" 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "_links":{ 36 | "next":{ 37 | "name":"activate", 38 | "href":"https://test.domain.com/api/v1/authn/factors/mbljbyz1nyglPZm000h7/lifecycle/activate", 39 | "hints":{ 40 | "allow":[ 41 | "POST" 42 | ] 43 | } 44 | }, 45 | "cancel":{ 46 | "href":"https://test.domain.com/api/v1/authn/cancel", 47 | "hints":{ 48 | "allow":[ 49 | "POST" 50 | ] 51 | } 52 | }, 53 | "prev":{ 54 | "href":"https://test.domain.com/api/v1/authn/previous", 55 | "hints":{ 56 | "allow":[ 57 | "POST" 58 | ] 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/Resources/PASSWORD_RESET: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken": "005Oj4_rx1yAYP2MFNobMXlM2wJ3QEyzgifBd_T6Go", 3 | "expiresAt": "2017-03-29T21:35:47.000Z", 4 | "status": "PASSWORD_RESET", 5 | "recoveryType": "ACCOUNT_ACTIVATION", 6 | "_embedded": { 7 | "user": { 8 | "id": "00ub0oNGTSWTBKOLGLNR", 9 | "passwordChanged": "2015-09-08T20:14:45.000Z", 10 | "profile": { 11 | "login": "dade.murphy@example.com", 12 | "firstName": "Dade", 13 | "lastName": "Murphy", 14 | "locale": "en_US", 15 | "timeZone": "America/Los_Angeles" 16 | } 17 | }, 18 | "policy": { 19 | "expiration": { 20 | "passwordExpireDays": 5 21 | }, 22 | "complexity": { 23 | "minLength": 8, 24 | "minLowerCase": 1, 25 | "minUpperCase": 1, 26 | "minNumber": 1, 27 | "minSymbol": 0 28 | } 29 | } 30 | }, 31 | "_links": { 32 | "next": { 33 | "name": "resetPassword", 34 | "href": "https://yourOktaDomain/api/v1/authn/credentials/reset_password", 35 | "hints": { 36 | "allow": [ 37 | "POST" 38 | ] 39 | } 40 | }, 41 | "cancel": { 42 | "href": "https://yourOktaDomain/api/v1/authn/cancel", 43 | "hints": { 44 | "allow": [ 45 | "POST" 46 | ] 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/Mocks/OktaAuthHTTPClientMock.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | @testable import OktaAuthNative 15 | 16 | class OktaAuthHTTPClientMock: OktaHTTPRequestListenerProtocol { 17 | 18 | let data: Data? 19 | let httpResponse: HTTPURLResponse? 20 | var error: Error? 21 | var didSendRequest = false 22 | 23 | init(data: Data?, httpResponse: HTTPURLResponse?, error: Error?) { 24 | self.data = data 25 | self.httpResponse = httpResponse 26 | self.error = error 27 | } 28 | 29 | func sendRequest(_ request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { 30 | didSendRequest = true 31 | completionHandler(data, httpResponse, error) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/Mocks/OktaAuthStatus+Mocking.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | import OktaAuthNative 15 | 16 | extension OktaAuthStatus { 17 | var apiMock: OktaAPIMock! { 18 | return self.restApi as? OktaAPIMock 19 | } 20 | 21 | @discardableResult func setupApiMockFailure(from resourceName: String = "AuthenticationFailedError") -> OktaAPIMock! { 22 | let mock = OktaAPIMock(successCase: false, resourceName: resourceName)! 23 | self.restApi = mock 24 | return mock 25 | } 26 | 27 | @discardableResult func setupApiMockResponse(_ response: TestResponse ) -> OktaAPIMock! { 28 | let mock = OktaAPIMock(successCase: true, resourceName: response.rawValue)! 29 | self.restApi = mock 30 | return mock 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Mocks/OktaFactorResultProtocolMock.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | import OktaAuthNative 15 | 16 | class OktaFactorResultProtocolMock: OktaFactorResultProtocol { 17 | 18 | var response: OktaAPIRequest.Result? 19 | 20 | var changedStatus: OktaAuthStatus? 21 | var error: OktaError? 22 | var statusUpdate: OktaAPISuccessResponse.FactorResult? 23 | 24 | func handleFactorServerResponse( 25 | response: OktaAPIRequest.Result, 26 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 27 | onError: @escaping (OktaError) -> Void) 28 | { 29 | self.response = response 30 | 31 | if let changedStatus = changedStatus { 32 | onStatusChange(changedStatus) 33 | } else if let error = error { 34 | onError(error) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/Resources/MFA_ENROLL_ACTIVATE_CALL: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", 3 | "expiresAt": "2015-11-03T10:15:57.000Z", 4 | "status": "MFA_ENROLL_ACTIVATE", 5 | "relayState": "/myapp/some/deep/link/i/want/to/return/to", 6 | "_embedded": { 7 | "user": { 8 | "id": "00ub0oNGTSWTBKOLGLNR", 9 | "passwordChanged": "2015-09-08T20:14:45.000Z", 10 | "profile": { 11 | "login": "dade.murphy@example.com", 12 | "firstName": "Dade", 13 | "lastName": "Murphy", 14 | "locale": "en_US", 15 | "timeZone": "America/Los_Angeles" 16 | } 17 | }, 18 | "factor": { 19 | "id": "clf198rKSEWOSKRIVIFT", 20 | "factorType": "call", 21 | "provider": "OKTA", 22 | "profile": { 23 | "name": "OKTA_VERIFY", 24 | "email": "some@email.com", 25 | "phoneNumber": "+1 XXX-XXX-1337" 26 | } 27 | } 28 | }, 29 | "_links": { 30 | "next": { 31 | "name": "activate", 32 | "href": "https://test.domain.com/api/v1/authn/factors/clf198rKSEWOSKRIVIFT/lifecycle/activate", 33 | "hints": { 34 | "allow": [ 35 | "POST" 36 | ] 37 | } 38 | }, 39 | "cancel": { 40 | "href": "https://test.domain.com/api/v1/authn/cancel", 41 | "hints": { 42 | "allow": [ 43 | "POST" 44 | ] 45 | } 46 | }, 47 | "prev": { 48 | "href": "https://test.domain.com/api/v1/authn/previous", 49 | "hints": { 50 | "allow": [ 51 | "POST" 52 | ] 53 | } 54 | }, 55 | "resend": [ 56 | { 57 | "name": "call", 58 | "href": "https://test.domain.com/api/v1/authn/factors/clf198rKSEWOSKRIVIFT/lifecycle/resend", 59 | "hints": { 60 | "allow": [ 61 | "POST" 62 | ] 63 | } 64 | } 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | general-platform-helpers: okta/general-platform-helpers@1.9.4 5 | 6 | executors: 7 | apple-ci-arm-medium: 8 | macos: 9 | xcode: 14.3.1 10 | resource_class: macos.m1.medium.gen1 11 | 12 | jobs: 13 | setup: 14 | executor: apple-ci-arm-medium 15 | steps: 16 | - checkout 17 | - persist_to_workspace: 18 | root: ~/project 19 | paths: 20 | - . 21 | 22 | snyk-scan: 23 | executor: apple-ci-arm-medium 24 | steps: 25 | - attach_workspace: 26 | at: ~/project 27 | - run: 28 | name: Install rosetta # Needed for snyk to work on M1 machines. 29 | command: softwareupdate --install-rosetta --agree-to-license 30 | - run: 31 | name: run swift package show dependencies 32 | command: swift package show-dependencies 33 | - general-platform-helpers/step-load-dependencies 34 | - general-platform-helpers/step-run-snyk-monitor: 35 | scan-all-projects: true 36 | skip-unresolved: false 37 | run-on-non-main: true 38 | os: macos 39 | 40 | workflows: 41 | security-scan: 42 | jobs: 43 | - setup: 44 | filters: 45 | branches: 46 | only: 47 | - master 48 | - snyk-scan: 49 | name: execute-snyk 50 | filters: 51 | branches: 52 | only: 53 | - master 54 | context: 55 | - static-analysis 56 | requires: 57 | - setup 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug you encountered with the Okta Auth SDK 3 | labels: [ bug ] 4 | body: 5 | - type: textarea 6 | id: problem 7 | attributes: 8 | label: Describe the bug? 9 | description: | 10 | Please be as detailed as possible. This will help us address the bug in a timely manner. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: expected 16 | attributes: 17 | label: What is expected to happen? 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: actual 23 | attributes: 24 | label: What is the actual behavior? 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: repro 30 | attributes: 31 | label: Reproduction Steps? 32 | description: | 33 | Please provide as much detail as possible to help us reproduce your bug. 34 | A reproduction repo is very helpful for us as well. 35 | validations: 36 | required: true 37 | 38 | - type: textarea 39 | id: additional 40 | attributes: 41 | label: Additional Information? 42 | 43 | - type: textarea 44 | id: sdkVersion 45 | attributes: 46 | label: SDK Version(s) 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | id: buildInformation 52 | attributes: 53 | label: Build Information 54 | description: If this is an issue related to building, please supply Xcode version, target device type / version, and any other relevant information. 55 | -------------------------------------------------------------------------------- /Example/Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Tests/Resources/PASSWORD_WARN: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", 3 | "expiresAt": "2015-11-03T10:15:57.000Z", 4 | "status": "PASSWORD_WARN", 5 | "relayState": "/myapp/some/deep/link/i/want/to/return/to", 6 | "_embedded": { 7 | "user": { 8 | "id": "00ub0oNGTSWTBKOLGLNR", 9 | "passwordChanged": "2015-09-08T20:14:45.000Z", 10 | "profile": { 11 | "login": "dade.murphy@example.com", 12 | "firstName": "Dade", 13 | "lastName": "Murphy", 14 | "locale": "en_US", 15 | "timeZone": "America/Los_Angeles" 16 | } 17 | }, 18 | "policy": { 19 | "expiration": { 20 | "passwordExpireDays": 5 21 | }, 22 | "complexity": { 23 | "minLength": 8, 24 | "minLowerCase": 1, 25 | "minUpperCase": 1, 26 | "minNumber": 1, 27 | "minSymbol": 0, 28 | "excludeUsername": true 29 | }, 30 | "age":{ 31 | "minAgeMinutes":0, 32 | "historyCount":0 33 | } 34 | } 35 | }, 36 | "_links": { 37 | "next": { 38 | "name": "changePassword", 39 | "href": "https://okta.okta.com/api/v1/authn/credentials/change_password", 40 | "hints": { 41 | "allow": [ 42 | "POST" 43 | ] 44 | } 45 | }, 46 | "skip": { 47 | "name": "skip", 48 | "href": "https://okta.okta.com/api/v1/authn/skip", 49 | "hints": { 50 | "allow": [ 51 | "POST" 52 | ] 53 | } 54 | }, 55 | "cancel": { 56 | "href": "https://okta.okta.com/api/v1/authn/cancel", 57 | "hints": { 58 | "allow": [ 59 | "POST" 60 | ] 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/Source/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests/Resources/Questions: -------------------------------------------------------------------------------- 1 | [{"question":"name_of_first_plush_toy","questionText":"What is the name of your first stuffed animal?"},{"question":"first_award","questionText":"What did you earn your first medal or award for?"},{"question":"favorite_security_question","questionText":"What is your favorite security question?"},{"question":"favorite_toy","questionText":"What is the toy/stuffed animal you liked the most as a kid?"},{"question":"first_computer_game","questionText":"What was the first computer game you played?"},{"question":"favorite_movie_quote","questionText":"What is your favorite movie quote?"},{"question":"first_sports_team_mascot","questionText":"What was the mascot of the first sports team you played on?"},{"question":"first_music_purchase","questionText":"What music album or song did you first purchase?"},{"question":"favorite_art_piece","questionText":"What is your favorite piece of art?"},{"question":"grandmother_favorite_desert","questionText":"What was your grandmother's favorite dessert?"},{"question":"first_thing_cooked","questionText":"What was the first thing you learned to cook?"},{"question":"childhood_dream_job","questionText":"What was your dream job as a child?"},{"question":"place_where_significant_other_was_met","questionText":"Where did you meet your spouse/significant other?"},{"question":"favorite_vacation_location","questionText":"Where did you go for your favorite vacation?"},{"question":"new_years_two_thousand","questionText":"Where were you on New Year's Eve in the year 2000?"},{"question":"favorite_speaker_actor","questionText":"Who is your favorite speaker/orator?"},{"question":"favorite_book_movie_character","questionText":"Who is your favorite book/movie character?"},{"question":"favorite_sports_player","questionText":"Who is your favorite sports player?"}] 2 | -------------------------------------------------------------------------------- /Tests/Resources/MFA_CHALLENGE_TOTP: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"00mv-_JFjDYoBJdKRVgXHX0e-BZxWouM9jcRj_EsyC", 3 | "expiresAt":"2019-04-18T15:53:15.000Z", 4 | "status":"MFA_CHALLENGE", 5 | "_embedded":{ 6 | "user":{ 7 | "id":"00ujbyedtoxuuXn9l0h7", 8 | "passwordChanged":"2019-02-11T14:09:22.000Z", 9 | "profile":{ 10 | "login":"ayurok", 11 | "firstName":"ayurok", 12 | "lastName":"ayurok", 13 | "locale":"en", 14 | "timeZone":"America/Los_Angeles" 15 | } 16 | }, 17 | "factor":{ 18 | "id":"ostkdh5wmlQpOvaoa0h7", 19 | "factorType":"token:software:totp", 20 | "provider":"OKTA", 21 | "vendorName":"OKTA", 22 | "profile":{ 23 | "credentialId":"ayurok" 24 | } 25 | }, 26 | "policy":{ 27 | "allowRememberDevice":false, 28 | "rememberDeviceLifetimeInMinutes":0, 29 | "rememberDeviceByDefault":false, 30 | "factorsPolicyInfo":null 31 | } 32 | }, 33 | "_links":{ 34 | "next":{ 35 | "name":"verify", 36 | "href":"https://test.domain.com/api/v1/authn/factors/ostkdh5wmlQpOvaoa0h7/verify", 37 | "hints":{ 38 | "allow":[ 39 | "POST" 40 | ] 41 | } 42 | }, 43 | "cancel":{ 44 | "href":"https://test.domain.com/api/v1/authn/cancel", 45 | "hints":{ 46 | "allow":[ 47 | "POST" 48 | ] 49 | } 50 | }, 51 | "prev":{ 52 | "href":"https://test.domain.com/api/v1/authn/previous", 53 | "hints":{ 54 | "allow":[ 55 | "POST" 56 | ] 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | .DS_Store 70 | -------------------------------------------------------------------------------- /Source/RestAPI/Utils.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | #if os(iOS) || os(watchOS) || os(tvOS) 16 | import UIKit 17 | #endif 18 | 19 | public func sdkVersion() -> String { 20 | return "2.4.6" 21 | } 22 | 23 | internal func buildUserAgent() -> String { 24 | let version = sdkVersion() 25 | let device = "Device/\(deviceModel())" 26 | #if os(iOS) 27 | let os = "iOS/\(UIDevice.current.systemVersion)" 28 | #elseif os(watchOS) 29 | let os = "watchOS/\(UIDevice.current.systemVersion)" 30 | #elseif os(tvOS) 31 | let os = "tvOS/\(UIDevice.current.systemVersion)" 32 | #elseif os(macOS) 33 | let osVersion = ProcessInfo.processInfo.operatingSystemVersion 34 | let os = "macOS/\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" 35 | #endif 36 | let string = "okta-auth-swift/\(version) \(os) \(device)" 37 | return string 38 | } 39 | 40 | internal func deviceModel() -> String { 41 | var system = utsname() 42 | uname(&system) 43 | let model = withUnsafePointer(to: &system.machine.0) { ptr in 44 | return String(cString: ptr) 45 | } 46 | return model 47 | } 48 | -------------------------------------------------------------------------------- /Tests/DomainObjects/AuthStateTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | 15 | @testable import OktaAuthNative 16 | 17 | class AuthStateTests: XCTestCase { 18 | 19 | func testAuthStatus() { 20 | let statuses: [AuthStatus] = [ 21 | .unauthenticated, 22 | .passwordWarning, 23 | .passwordExpired, 24 | .recovery, 25 | .recoveryChallenge, 26 | .passwordReset, 27 | .lockedOut, 28 | .MFAEnroll, 29 | .MFAEnrollActivate, 30 | .MFARequired, 31 | .MFAChallenge, 32 | .success, 33 | .unknown("test") 34 | ] 35 | 36 | let encoder = JSONEncoder() 37 | let decoder = JSONDecoder() 38 | 39 | for status in statuses { 40 | do { 41 | let encodedData = try encoder.encode([status]) 42 | let decodedStatus = (try decoder.decode([AuthStatus].self, from: encodedData)).first 43 | 44 | XCTAssertEqual(status, decodedStatus) 45 | } catch { 46 | XCTFail(error.localizedDescription) 47 | continue 48 | } 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Tests/Resources/MFA_ENROLL_ACTIVATE_SMS: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"001k7sl4Ts3ZLV1yx-eC00eJbOUxL8Vfl77gAKwS_B", 3 | "expiresAt":"2019-04-18T15:33:50.000Z", 4 | "status":"MFA_ENROLL_ACTIVATE", 5 | "_embedded":{ 6 | "user":{ 7 | "id":"00ujbyedtoxuuXn9l0h7", 8 | "passwordChanged":"2019-02-11T14:09:22.000Z", 9 | "profile":{ 10 | "login":"ayurok", 11 | "firstName":"ayurok", 12 | "lastName":"ayurok", 13 | "locale":"en", 14 | "timeZone":"America/Los_Angeles" 15 | } 16 | }, 17 | "factor":{ 18 | "id":"mbljbyz1nyglPZm000h7", 19 | "factorType":"sms", 20 | "provider":"OKTA", 21 | "vendorName":"OKTA", 22 | "profile":{ 23 | "phoneNumber":"+555 XX XXX 5555" 24 | } 25 | } 26 | }, 27 | "_links":{ 28 | "next":{ 29 | "name":"activate", 30 | "href":"https://test.domain.com/api/v1/authn/factors/mbljbyz1nyglPZm000h7/lifecycle/activate", 31 | "hints":{ 32 | "allow":[ 33 | "POST" 34 | ] 35 | } 36 | }, 37 | "cancel":{ 38 | "href":"https://test.domain.com/api/v1/authn/cancel", 39 | "hints":{ 40 | "allow":[ 41 | "POST" 42 | ] 43 | } 44 | }, 45 | "prev":{ 46 | "href":"https://test.domain.com/api/v1/authn/previous", 47 | "hints":{ 48 | "allow":[ 49 | "POST" 50 | ] 51 | } 52 | }, 53 | "resend":[ 54 | { 55 | "name":"sms", 56 | "href":"https://test.domain.com/api/v1/authn/factors/mbljbyz1nyglPZm000h7/lifecycle/resend", 57 | "hints":{ 58 | "allow":[ 59 | "POST" 60 | ] 61 | } 62 | } 63 | ] 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Source/Factors/OktaFactorOther.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaFactorOther : OktaFactor { 16 | 17 | public func sendRequest(with link: LinksResponse.Link, 18 | keyValuePayload: Dictionary, 19 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 20 | onError: @escaping (OktaError) -> Void) { 21 | self.restApi?.sendApiRequest(with: link, 22 | bodyParams: keyValuePayload, 23 | method: .post, 24 | completion: { result in 25 | self.handleServerResponse(response: result, 26 | onStatusChange: onStatusChange, 27 | onError: onError) 28 | }) 29 | } 30 | 31 | // MARK: - Internal 32 | override init(factor: EmbeddedResponse.Factor, 33 | stateToken:String, 34 | verifyLink: LinksResponse.Link?, 35 | activationLink: LinksResponse.Link?) { 36 | super.init(factor: factor, stateToken: stateToken, verifyLink: verifyLink, activationLink: activationLink) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/Mocks/OktaAuthStatusResponseHandlerMock.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | import OktaAuthNative 15 | 16 | class OktaAuthStatusResponseHandlerMock: OktaAuthStatusResponseHandler { 17 | 18 | var changedStatus: OktaAuthStatus? 19 | var error: OktaError? 20 | var statusUpdate: OktaAPISuccessResponse.FactorResult? 21 | 22 | var handleResponseCalled: Bool = false 23 | var response: OktaAPIRequest.Result? 24 | 25 | init(changedStatus: OktaAuthStatus? = nil, error: OktaError? = nil, statusUpdate: OktaAPISuccessResponse.FactorResult? = nil) { 26 | super.init() 27 | self.changedStatus = changedStatus 28 | self.error = error 29 | self.statusUpdate = statusUpdate 30 | } 31 | 32 | override func handleServerResponse(_ response: OktaAPIRequest.Result, 33 | currentStatus: OktaAuthStatus, 34 | onStatusChanged: @escaping (OktaAuthStatus) -> Void, 35 | onError: @escaping (OktaError) -> Void) { 36 | self.handleResponseCalled = true 37 | self.response = response 38 | 39 | if let changedStatus = changedStatus { 40 | onStatusChanged(changedStatus) 41 | return 42 | } 43 | 44 | if let error = error { 45 | onError(error) 46 | return 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/Statuses/OktaAuthStatusSuccessTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | @testable import OktaAuthNative 15 | 16 | class OktaAuthStatusSuccessTests: XCTestCase { 17 | 18 | func testSuccessSignIn() { 19 | guard let status = createStatus() else { 20 | XCTFail() 21 | return 22 | } 23 | 24 | XCTAssertNotNil(status.sessionToken) 25 | XCTAssertNil(status.recoveryType) 26 | } 27 | 28 | func testSuccessUnlock() { 29 | guard let status = createStatus(from: OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 30 | withResponse: .SUCCESS_UNLOCK) else { 31 | XCTFail() 32 | return 33 | } 34 | 35 | XCTAssertNil(status.sessionToken) 36 | XCTAssertNotNil(status.recoveryType) 37 | } 38 | 39 | // MARK: - Utils 40 | 41 | func createStatus( 42 | from currentStatus: OktaAuthStatus = OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 43 | withResponse response: TestResponse = .SUCCESS) 44 | -> OktaAuthStatusSuccess? { 45 | 46 | guard let response = response.parse() else { 47 | return nil 48 | } 49 | 50 | return try? OktaAuthStatusSuccess(currentState: currentStatus, model: response) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Example/Source/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Tests/Resources/MFA_CHALLENGE_SMS: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"00growRoFx5zmifaZ4lCmfkWqelsik56Q4Czch_mVp", 3 | "expiresAt":"2019-04-18T15:47:14.000Z", 4 | "status":"MFA_CHALLENGE", 5 | "_embedded":{ 6 | "user":{ 7 | "id":"00ujbyedtoxuuXn9l0h7", 8 | "passwordChanged":"2019-02-11T14:09:22.000Z", 9 | "profile":{ 10 | "login":"ayurok", 11 | "firstName":"ayurok", 12 | "lastName":"ayurok", 13 | "locale":"en", 14 | "timeZone":"America/Los_Angeles" 15 | } 16 | }, 17 | "factor":{ 18 | "id":"smskdhbk0ajTQ7ZyD0h7", 19 | "factorType":"sms", 20 | "provider":"OKTA", 21 | "vendorName":"OKTA", 22 | "profile":{ 23 | "phoneNumber":"+555 XX XXX 5555" 24 | } 25 | }, 26 | "policy":{ 27 | "allowRememberDevice":false, 28 | "rememberDeviceLifetimeInMinutes":0, 29 | "rememberDeviceByDefault":false, 30 | "factorsPolicyInfo":null 31 | } 32 | }, 33 | "_links":{ 34 | "next":{ 35 | "name":"verify", 36 | "href":"https://test.domain.com/api/v1/authn/factors/smskdhbk0ajTQ7ZyD0h7/verify", 37 | "hints":{ 38 | "allow":[ 39 | "POST" 40 | ] 41 | } 42 | }, 43 | "cancel":{ 44 | "href":"https://test.domain.com/api/v1/authn/cancel", 45 | "hints":{ 46 | "allow":[ 47 | "POST" 48 | ] 49 | } 50 | }, 51 | "resend":[ 52 | { 53 | "name":"sms", 54 | "href":"https://test.domain.com/api/v1/authn/factors/smskdhbk0ajTQ7ZyD0h7/verify/resend", 55 | "hints":{ 56 | "allow":[ 57 | "POST" 58 | ] 59 | } 60 | } 61 | ], 62 | "prev":{ 63 | "href":"https://test.domain.com/api/v1/authn/previous", 64 | "hints":{ 65 | "allow":[ 66 | "POST" 67 | ] 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusLockedOut.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusLockedOut : OktaAuthStatus { 16 | 17 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 18 | try super.init(currentState: currentState, model: model) 19 | statusType = .lockedOut 20 | } 21 | 22 | open func canUnlock() -> Bool { 23 | guard model.links?.next?.href != nil else { 24 | return false 25 | } 26 | 27 | return true 28 | } 29 | 30 | open func unlock(username: String, 31 | factorType: OktaRecoveryFactors, 32 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 33 | onError: @escaping (_ error: OktaError) -> Void) { 34 | guard canUnlock() else { 35 | onError(.wrongStatus("Can't find 'next' link in response")) 36 | return 37 | } 38 | 39 | do { 40 | let unauthenticated = try OktaAuthStatusUnauthenticated(currentState: self, model: self.model) 41 | unauthenticated.unlockAccount(username: username, 42 | factorType: factorType, 43 | onStatusChange: onStatusChange, 44 | onError: onError) 45 | } catch let error { 46 | onError(error as! OktaError) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Source/Factors/OktaFactorSms.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaFactorSms : OktaFactor { 16 | 17 | public var phoneNumber: String? { 18 | get { 19 | return factor.profile?.phoneNumber 20 | } 21 | } 22 | 23 | public func enroll(phoneNumber: String, 24 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 25 | onError: @escaping (OktaError) -> Void) { 26 | self.enroll(questionId: nil, 27 | answer: nil, 28 | credentialId: nil, 29 | passCode: nil, 30 | phoneNumber: phoneNumber, 31 | onStatusChange: onStatusChange, 32 | onError: onError) 33 | } 34 | 35 | public func verify(passCode: String, 36 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 37 | onError: @escaping (_ error: OktaError) -> Void) { 38 | super.verify(passCode: passCode, 39 | answerToSecurityQuestion: nil, 40 | onStatusChange: onStatusChange, 41 | onError: onError) 42 | } 43 | 44 | // MARK: - Internal 45 | override init(factor: EmbeddedResponse.Factor, 46 | stateToken:String, 47 | verifyLink: LinksResponse.Link?, 48 | activationLink: LinksResponse.Link?) { 49 | super.init(factor: factor, stateToken: stateToken, verifyLink: verifyLink, activationLink: activationLink) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Source/Factors/OktaFactorCall.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaFactorCall : OktaFactor { 16 | 17 | public var phoneNumber: String? { 18 | get { 19 | return factor.profile?.phoneNumber 20 | } 21 | } 22 | 23 | public func enroll(phoneNumber: String, 24 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 25 | onError: @escaping (OktaError) -> Void) { 26 | self.enroll(questionId: nil, 27 | answer: nil, 28 | credentialId: nil, 29 | passCode: nil, 30 | phoneNumber: phoneNumber, 31 | onStatusChange: onStatusChange, 32 | onError: onError) 33 | } 34 | 35 | public func verify(passCode: String, 36 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 37 | onError: @escaping (_ error: OktaError) -> Void) { 38 | super.verify(passCode: passCode, 39 | answerToSecurityQuestion: nil, 40 | onStatusChange: onStatusChange, 41 | onError: onError) 42 | } 43 | 44 | // MARK: - Internal 45 | override init(factor: EmbeddedResponse.Factor, 46 | stateToken:String, 47 | verifyLink: LinksResponse.Link?, 48 | activationLink: LinksResponse.Link?) { 49 | super.init(factor: factor, stateToken: stateToken, verifyLink: verifyLink, activationLink: activationLink) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/Mocks/OktaFactor+Mocking.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | import OktaAuthNative 15 | 16 | extension OktaFactor { 17 | 18 | // MARK: - OktaFactorResultProtocolMock 19 | 20 | @discardableResult func setupMockDelegate() -> OktaFactorResultProtocolMock { 21 | let delegate = OktaFactorResultProtocolMock() 22 | responseDelegate = delegate 23 | return delegate 24 | } 25 | 26 | @discardableResult func setupMockDelegate(with error: OktaError) -> OktaFactorResultProtocolMock { 27 | let delegate = setupMockDelegate() 28 | delegate.error = error 29 | return delegate 30 | } 31 | 32 | @discardableResult func setupMockDelegate(with changedStatus: OktaAuthStatus) -> OktaFactorResultProtocolMock { 33 | let delegate = setupMockDelegate() 34 | delegate.changedStatus = changedStatus 35 | return delegate 36 | } 37 | 38 | // MARK: - OktaAPIMock 39 | 40 | var apiMock: OktaAPIMock! { 41 | return self.restApi as? OktaAPIMock 42 | } 43 | 44 | @discardableResult func setupApiMockFailure(from resourceName: String = "AuthenticationFailedError") -> OktaAPIMock! { 45 | let mock = OktaAPIMock(successCase: false, resourceName: resourceName)! 46 | self.restApi = mock 47 | return mock 48 | } 49 | 50 | @discardableResult func setupApiMockResponse(_ response: TestResponse ) -> OktaAPIMock! { 51 | let mock = OktaAPIMock(successCase: true, resourceName: response.rawValue)! 52 | self.restApi = mock 53 | return mock 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusPasswordExpired.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusPasswordExpired : OktaAuthStatus { 16 | 17 | public internal(set) var stateToken: String 18 | 19 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 20 | guard let stateToken = model.stateToken else { 21 | throw OktaError.invalidResponse("State token is missed") 22 | } 23 | self.stateToken = stateToken 24 | try super.init(currentState: currentState, model: model) 25 | statusType = .passwordExpired 26 | } 27 | 28 | open func canChange() -> Bool { 29 | 30 | guard (model.links?.next?.href) != nil else { 31 | return false 32 | } 33 | 34 | return true 35 | } 36 | 37 | open func changePassword(oldPassword: String, 38 | newPassword: String, 39 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 40 | onError: @escaping (_ error: OktaError) -> Void) { 41 | 42 | guard canChange() else { 43 | onError(.wrongStatus("Can't find 'next' link in response")) 44 | return 45 | } 46 | 47 | restApi.changePassword(link: model.links!.next!, 48 | stateToken: stateToken, 49 | oldPassword: oldPassword, 50 | newPassword: newPassword) { result in 51 | 52 | self.handleServerResponse(result, 53 | onStatusChanged: onStatusChange, 54 | onError: onError) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/Utils/TestResponse.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | @testable import OktaAuthNative 16 | 17 | enum TestResponse: String { 18 | case MFA_ENROLL_NotEnrolled = "MFA_ENROLL_NotEnrolled" 19 | case MFA_ENROLL_PartiallyEnrolled = "MFA_ENROLL_PartiallyEnrolled" 20 | case MFA_ENROLL_ACTIVATE_SMS = "MFA_ENROLL_ACTIVATE_SMS" 21 | case MFA_ENROLL_ACTIVATE_CALL = "MFA_ENROLL_ACTIVATE_CALL" 22 | case MFA_ENROLL_ACTIVATE_Push = "MFA_ENROLL_ACTIVATE_Push" 23 | case MFA_ENROLL_ACTIVATE_TOTP = "MFA_ENROLL_ACTIVATE_TOTP" 24 | case MFA_REQUIRED = "MFA_REQUIRED" 25 | case MFA_CHALLENGE_SMS = "MFA_CHALLENGE_SMS" 26 | case MFA_CHALLENGE_TOTP = "MFA_CHALLENGE_TOTP" 27 | case MFA_CHALLENGE_WAITING_PUSH = "MFA_CHALLENGE_WAITING_PUSH" 28 | case SUCCESS = "SUCCESS" 29 | case SUCCESS_UNLOCK = "SUCCESS_UNLOCK" 30 | case PASSWORD_WARNING = "PASSWORD_WARN" 31 | case PASSWORD_EXPIRED = "PASSWORD_EXPIRED" 32 | case PASSWORD_RESET = "PASSWORD_RESET" 33 | case LOCKED_OUT = "LOCKED_OUT" 34 | case RECOVERY = "RECOVERY" 35 | case RECOVERY_CHALLENGE_SMS = "RECOVERY_CHALLENGE_SMS" 36 | case RECOVERY_CHALLENGE_EMAIL = "RECOVERY_CHALLENGE_EMAIL" 37 | case Unknown_State_And_FactorResult = "Unknown_State_And_FactorResult" 38 | 39 | func data() -> Data? { 40 | return OktaAPIMock.dataFor(resource: self.rawValue) 41 | } 42 | 43 | func parse() -> OktaAPISuccessResponse? { 44 | guard let data = data() else { 45 | return nil 46 | } 47 | 48 | let decoder = JSONDecoder() 49 | let formatter = DateFormatter() 50 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 51 | decoder.dateDecodingStrategy = .formatted(formatter) 52 | 53 | return try? decoder.decode(OktaAPISuccessResponse.self, from: data) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/DomainObjects/TypesTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | 15 | @testable import OktaAuthNative 16 | 17 | class TypesTests: XCTestCase { 18 | 19 | func testFactorType() { 20 | let factors: [FactorType] = [ 21 | .question, .sms, .call, .TOTP, .push, .token, .tokenHardware, .web, .u2f, .email, .unknown("test") 22 | ] 23 | 24 | let encoder = JSONEncoder() 25 | let decoder = JSONDecoder() 26 | 27 | for factor in factors { 28 | do { 29 | let encodedData = try encoder.encode([factor]) 30 | let decodedFactor = (try decoder.decode([FactorType].self, from: encodedData)).first 31 | 32 | XCTAssertEqual(factor, decodedFactor) 33 | } catch { 34 | XCTFail(error.localizedDescription) 35 | continue 36 | } 37 | } 38 | } 39 | 40 | func testFactorProvider() { 41 | let providers: [FactorProvider] = [ 42 | .okta, .google, .rsa, .symantec, .yubico, .duo, .fido, .unknown("test") 43 | ] 44 | 45 | let encoder = JSONEncoder() 46 | let decoder = JSONDecoder() 47 | 48 | for provider in providers { 49 | do { 50 | let encodedData = try encoder.encode([provider]) 51 | let decodedProvider = (try decoder.decode([FactorProvider].self, from: encodedData)).first 52 | 53 | XCTAssertEqual(provider, decodedProvider) 54 | } catch { 55 | XCTFail(error.localizedDescription) 56 | continue 57 | } 58 | } 59 | } 60 | 61 | func testOktaRecoveryFactors() { 62 | XCTAssertEqual(OktaRecoveryFactors.email.toFactorType(), FactorType.email) 63 | XCTAssertEqual(OktaRecoveryFactors.call.toFactorType(), FactorType.call) 64 | XCTAssertEqual(OktaRecoveryFactors.sms.toFactorType(), FactorType.sms) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatePasswordReset.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusPasswordReset : OktaAuthStatus { 16 | 17 | public internal(set) var stateToken: String 18 | 19 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 20 | guard let stateToken = model.stateToken else { 21 | throw OktaError.invalidResponse("State token is missed") 22 | } 23 | self.stateToken = stateToken 24 | if let policy = model.embedded?.policy { 25 | switch policy { 26 | case .password(let password): 27 | passwordExpiration = password.expiration 28 | passwordComplexity = password.complexity 29 | default: 30 | break 31 | } 32 | } 33 | try super.init(currentState: currentState, model: model) 34 | statusType = .passwordReset 35 | } 36 | 37 | open var passwordExpiration: EmbeddedResponse.Policy.Password.PasswordExpiration? 38 | 39 | open var passwordComplexity: EmbeddedResponse.Policy.Password.PasswordComplexity? 40 | 41 | open func canReset() -> Bool { 42 | 43 | guard model.links?.next?.href != nil else { 44 | return false 45 | } 46 | 47 | return true 48 | } 49 | 50 | open func resetPassword(newPassword: String, 51 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 52 | onError: @escaping (_ error: OktaError) -> Void) { 53 | guard canReset() else { 54 | onError(.wrongStatus("Can't find 'next' link in response")) 55 | return 56 | } 57 | 58 | restApi.resetPassword(newPassword: newPassword, stateToken: stateToken, link: model.links!.next!) { result in 59 | self.handleServerResponse(result, 60 | onStatusChanged: onStatusChange, 61 | onError: onError) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Source/Factors/OktaFactorToken.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaFactorToken : OktaFactor { 16 | 17 | public var credentialId: String? { 18 | get { 19 | return factor.profile?.credentialId 20 | } 21 | } 22 | 23 | public var factorProvider: FactorProvider? { 24 | get { 25 | return factor.provider 26 | } 27 | } 28 | 29 | public func enroll(credentialId: String, 30 | passCode: String, 31 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 32 | onError: @escaping (OktaError) -> Void) { 33 | super.enroll(questionId: nil, 34 | answer: nil, 35 | credentialId: credentialId, 36 | passCode: passCode, 37 | phoneNumber: nil, 38 | onStatusChange: onStatusChange, 39 | onError: onError) 40 | } 41 | 42 | public func select(passCode: String, 43 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 44 | onError: @escaping (OktaError) -> Void) { 45 | self.verify(passCode: passCode, 46 | onStatusChange: onStatusChange, 47 | onError: onError) 48 | } 49 | 50 | public func verify(passCode: String, 51 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 52 | onError: @escaping (_ error: OktaError) -> Void) { 53 | super.verify(passCode: passCode, 54 | answerToSecurityQuestion: nil, 55 | onStatusChange: onStatusChange, 56 | onError: onError) 57 | } 58 | 59 | // MARK: - Internal 60 | override init(factor: EmbeddedResponse.Factor, 61 | stateToken:String, 62 | verifyLink: LinksResponse.Link?, 63 | activationLink: LinksResponse.Link?) { 64 | super.init(factor: factor, stateToken: stateToken, verifyLink: verifyLink, activationLink: activationLink) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/Resources/MFA_ENROLL_ACTIVATE_Push: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"00_id3fUof3vX9Fy1H3qejz7izKh1gou0rOWkwJesi", 3 | "expiresAt":"2019-04-18T15:43:41.000Z", 4 | "status":"MFA_ENROLL_ACTIVATE", 5 | "factorResult":"WAITING", 6 | "_embedded":{ 7 | "user":{ 8 | "id":"00ujbyedtoxuuXn9l0h7", 9 | "passwordChanged":"2019-02-11T14:09:22.000Z", 10 | "profile":{ 11 | "login":"ayurok", 12 | "firstName":"ayurok", 13 | "lastName":"ayurok", 14 | "locale":"en", 15 | "timeZone":"America/Los_Angeles" 16 | } 17 | }, 18 | "factor":{ 19 | "id":"opfkdh40kws5XarDb0h7", 20 | "factorType":"push", 21 | "provider":"OKTA", 22 | "vendorName":"OKTA", 23 | "_embedded":{ 24 | "activation":{ 25 | "expiresAt":"2019-04-18T15:48:41.000Z", 26 | "factorResult":"WAITING", 27 | "_links":{ 28 | "qrcode":{ 29 | "href":"https://test.domain.com.com/api/v1/users/factors/qr/cQYp5xpm", 30 | "type":"image/png" 31 | }, 32 | "send":[ 33 | { 34 | "name":"email", 35 | "href":"https://test.domain.com.com/api/v1/authn/factors/lifecycle/activate/email", 36 | "hints":{ 37 | "allow":[ 38 | "POST" 39 | ] 40 | } 41 | }, 42 | { 43 | "name":"sms", 44 | "href":"https://test.domain.com.com/api/v1/authn/factors/lifecycle/activate/sms", 45 | "hints":{ 46 | "allow":[ 47 | "POST" 48 | ] 49 | } 50 | } 51 | ] 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | "_links":{ 58 | "next":{ 59 | "name":"activate", 60 | "href":"https://test.domain.com/api/v1/authn/factors/mbljbyz1nyglPZm000h7/lifecycle/activate", 61 | "hints":{ 62 | "allow":[ 63 | "POST" 64 | ] 65 | } 66 | }, 67 | "cancel":{ 68 | "href":"https://test.domain.com/api/v1/authn/cancel", 69 | "hints":{ 70 | "allow":[ 71 | "POST" 72 | ] 73 | } 74 | }, 75 | "prev":{ 76 | "href":"https://test.domain.com/api/v1/authn/previous", 77 | "hints":{ 78 | "allow":[ 79 | "POST" 80 | ] 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/Resources/Unknown_State_And_FactorResult: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"00_id3fUof3vX9Fy1H3qejz7izKh1gou0rOWkwJesi", 3 | "expiresAt":"2019-04-18T15:43:41.000Z", 4 | "status":"SOME_STATUS", 5 | "factorResult":"SOME_FACTOR_RESULT", 6 | "_embedded":{ 7 | "user":{ 8 | "id":"00ujbyedtoxuuXn9l0h7", 9 | "passwordChanged":"2019-02-11T14:09:22.000Z", 10 | "profile":{ 11 | "login":"ayurok", 12 | "firstName":"ayurok", 13 | "lastName":"ayurok", 14 | "locale":"en", 15 | "timeZone":"America/Los_Angeles" 16 | } 17 | }, 18 | "factor":{ 19 | "id":"opfkdh40kws5XarDb0h7", 20 | "factorType":"push", 21 | "provider":"OKTA", 22 | "vendorName":"OKTA", 23 | "_embedded":{ 24 | "activation":{ 25 | "expiresAt":"2019-04-18T15:48:41.000Z", 26 | "factorResult":"SOME_FACTOR_RESULT", 27 | "_links":{ 28 | "qrcode":{ 29 | "href":"https://test.domain.com.com/api/v1/users/factors/qr/cQYp5xpm", 30 | "type":"image/png" 31 | }, 32 | "send":[ 33 | { 34 | "name":"email", 35 | "href":"https://test.domain.com.com/api/v1/authn/factors/lifecycle/activate/email", 36 | "hints":{ 37 | "allow":[ 38 | "POST" 39 | ] 40 | } 41 | }, 42 | { 43 | "name":"sms", 44 | "href":"https://test.domain.com.com/api/v1/authn/factors/lifecycle/activate/sms", 45 | "hints":{ 46 | "allow":[ 47 | "POST" 48 | ] 49 | } 50 | } 51 | ] 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | "_links":{ 58 | "next":{ 59 | "name":"activate", 60 | "href":"https://test.domain.com/api/v1/authn/factors/mbljbyz1nyglPZm000h7/lifecycle/activate", 61 | "hints":{ 62 | "allow":[ 63 | "POST" 64 | ] 65 | } 66 | }, 67 | "cancel":{ 68 | "href":"https://test.domain.com/api/v1/authn/cancel", 69 | "hints":{ 70 | "allow":[ 71 | "POST" 72 | ] 73 | } 74 | }, 75 | "prev":{ 76 | "href":"https://test.domain.com/api/v1/authn/previous", 77 | "hints":{ 78 | "allow":[ 79 | "POST" 80 | ] 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/Resources/MFA_CHALLENGE_WAITING_PUSH: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"00lAftOt57ZSsQi13nbZmiPy_w4QSK_iKNX6XHOdRt", 3 | "expiresAt":"2019-04-18T15:52:09.000Z", 4 | "status":"MFA_CHALLENGE", 5 | "factorResult":"WAITING", 6 | "_embedded":{ 7 | "user":{ 8 | "id":"00ujbyedtoxuuXn9l0h7", 9 | "passwordChanged":"2019-02-11T14:09:22.000Z", 10 | "profile":{ 11 | "login":"ayurok", 12 | "firstName":"ayurok", 13 | "lastName":"ayurok", 14 | "locale":"en", 15 | "timeZone":"America/Los_Angeles" 16 | } 17 | }, 18 | "factor":{ 19 | "id":"opfkdh40kws5XarDb0h7", 20 | "factorType":"push", 21 | "provider":"OKTA", 22 | "vendorName":"OKTA", 23 | "profile":{ 24 | "credentialId":"ayurok", 25 | "deviceType":"SmartPhone_IPhone", 26 | "keys":[ 27 | { 28 | "kty":"PKIX", 29 | "use":"sig", 30 | "kid":"default", 31 | "x5c":[ 32 | "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrmoN/w5wgjB0Rd/z2O/4SVzV/Q/wjcizvfhtbvlZuX0KYnLyoWdpdruFNAz+HEcstTVPcRIL45qivVYCp4T7D6qTqsVlSTLoe638TqiEAYPpsqRuqbe7YWc404RtAXopNVre99Uu2H6b7l+aGNpW2TAPK5KNe0YpAEV0jIJBYFPYmoxHS+e1V1qRDm/NxggMV6w4vhR3bFJZYJJSoohSKHRimeHRRPofSfdDIFA4lDpkpmn1XtWwZF9EC8pD7JdVgdtBgtgQL1adfHDjckqaKbiftPV2whekthXoFDeO9MN7Ir4vRR+oktSGS2Ei8ISX7OwtzCk527GVTIHiHeQtwIDAQAB" 33 | ] 34 | } 35 | ], 36 | "name":"Anastasia Y.", 37 | "platform":"IOS", 38 | "version":"12.2" 39 | }, 40 | "_embedded":{ 41 | "challenge":{ 42 | "correctAnswer":92 43 | } 44 | } 45 | }, 46 | "policy":{ 47 | "allowRememberDevice":false, 48 | "rememberDeviceLifetimeInMinutes":0, 49 | "rememberDeviceByDefault":false, 50 | "factorsPolicyInfo":null 51 | } 52 | }, 53 | "_links":{ 54 | "next":{ 55 | "name":"poll", 56 | "href":"https://test.domain.com/api/v1/authn/factors/opfkdh40kws5XarDb0h7/verify", 57 | "hints":{ 58 | "allow":[ 59 | "POST" 60 | ] 61 | } 62 | }, 63 | "cancel":{ 64 | "href":"https://test.domain.com/api/v1/authn/cancel", 65 | "hints":{ 66 | "allow":[ 67 | "POST" 68 | ] 69 | } 70 | }, 71 | "resend":[ 72 | { 73 | "name":"push", 74 | "href":"https://test.domain.com/api/v1/authn/factors/opfkdh40kws5XarDb0h7/verify/resend", 75 | "hints":{ 76 | "allow":[ 77 | "POST" 78 | ] 79 | } 80 | } 81 | ], 82 | "prev":{ 83 | "href":"https://test.domain.com/api/v1/authn/previous", 84 | "hints":{ 85 | "allow":[ 86 | "POST" 87 | ] 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/Factors/OktaFactorTestCase.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | 14 | import XCTest 15 | @testable import OktaAuthNative 16 | 17 | class OktaFactorTestCase: XCTestCase { 18 | func createFactor(from response: TestResponse, type: FactorType) -> T? { 19 | guard let responseModel = response.parse(), 20 | let statetoken = responseModel.stateToken else { 21 | XCTFail("Unable to parse response!") 22 | return nil 23 | } 24 | 25 | guard let responseFactor: EmbeddedResponse.Factor = { 26 | if responseModel.embedded?.factor?.factorType == type { 27 | return responseModel.embedded?.factor 28 | } 29 | 30 | return responseModel.embedded?.factors?.first(where: { $0.factorType == type }) 31 | }() else { 32 | return nil 33 | } 34 | 35 | let verifyLink = responseModel.links?.verify ?? responseFactor.links?.verify 36 | let activationLink = (responseModel.links?.next?.name == "activate") ? responseModel.links?.next : nil 37 | 38 | return OktaFactor.createFactorWith(responseFactor, 39 | stateToken: statetoken, 40 | verifyLink: verifyLink, 41 | activationLink: activationLink) as? T 42 | } 43 | 44 | func verifyDelegateFailed(_ delegate: OktaFactorResultProtocolMock, with errorDescription: String? = nil) { 45 | guard let delegateResponse = delegate.response, 46 | case .error(let error) = delegateResponse else { 47 | XCTFail("Delegate should be called with error response!") 48 | return 49 | } 50 | 51 | guard let expectedError = errorDescription else { 52 | return 53 | } 54 | 55 | XCTAssertEqual(expectedError, error.localizedDescription) 56 | } 57 | 58 | func verifyDelegateSucceeded(_ delegate: OktaFactorResultProtocolMock, with expectedResponse: TestResponse) { 59 | guard let delegateResponse = delegate.response, 60 | case .success(let response) = delegateResponse else { 61 | XCTFail("Delegate should be called with success response!") 62 | return 63 | } 64 | 65 | XCTAssertEqual(expectedResponse.parse()?.rawData, response.rawData) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Source/OktaError.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | public enum OktaError: LocalizedError { 16 | case errorBuildingURLRequest(String) 17 | case connectionError(Error) 18 | case emptyServerResponse 19 | case invalidResponse(String) 20 | case responseSerializationError(Error, Data) 21 | case serverRespondedWithError(OktaAPIErrorResponse) 22 | case unexpectedResponse 23 | case wrongStatus(String) 24 | case alreadyInProgress 25 | case unknownStatus(OktaAPISuccessResponse) 26 | case internalError(String) 27 | case invalidParameters(String) 28 | } 29 | 30 | public extension OktaError { 31 | var description: String { 32 | switch self { 33 | case let .errorBuildingURLRequest(reason): 34 | return "Error building URL request.\nReason Failure: \(reason)." 35 | case .connectionError(let error): 36 | return "Connection error (\(error.localizedDescription))" 37 | case .emptyServerResponse: 38 | return "Empty server response" 39 | case let .invalidResponse(error): 40 | return "Invalid server response: \(error)" 41 | case .responseSerializationError(let error, _): 42 | return "Response serialization error (\(error.localizedDescription))" 43 | case .serverRespondedWithError(let error): 44 | let description: String 45 | if let causes = error.errorCauses, causes.count > 0 { 46 | description = causes.compactMap { $0.errorSummary }.joined(separator: "; ") 47 | } else if let summary = error.errorSummary { 48 | description = summary 49 | } else { 50 | description = "Unknown" 51 | } 52 | return "Server responded with error: \(description)" 53 | case .unexpectedResponse: 54 | return "Unexpected response" 55 | case .wrongStatus(error: let error): 56 | return error 57 | case .alreadyInProgress: 58 | return "Another request is in progress" 59 | case .unknownStatus: 60 | return "Received state is unknown" 61 | case let .internalError(error): 62 | return "Internal error: \(error)" 63 | case let .invalidParameters(error): 64 | return "Invalid parameters: \(error)" 65 | } 66 | } 67 | 68 | var localizedDescription: String { 69 | NSLocalizedString(description, comment: "") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusPasswordWarning.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusPasswordWarning : OktaAuthStatus { 16 | 17 | public internal(set) var stateToken: String 18 | 19 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 20 | guard let stateToken = model.stateToken else { 21 | throw OktaError.invalidResponse("State token is missed") 22 | } 23 | self.stateToken = stateToken 24 | try super.init(currentState: currentState, model: model) 25 | statusType = .passwordWarning 26 | } 27 | 28 | open func canChange() -> Bool { 29 | 30 | guard (model.links?.next?.href) != nil else { 31 | return false 32 | } 33 | 34 | return true 35 | } 36 | 37 | open func canSkip() -> Bool { 38 | 39 | guard (model.links?.skip?.href) != nil else { 40 | return false 41 | } 42 | 43 | return true 44 | } 45 | 46 | open func changePassword(oldPassword: String, 47 | newPassword: String, 48 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 49 | onError: @escaping (_ error: OktaError) -> Void) { 50 | do { 51 | let changePasswordStatus = try OktaAuthStatusPasswordExpired(currentState: self, model: self.model) 52 | changePasswordStatus.changePassword(oldPassword: oldPassword, 53 | newPassword: newPassword, 54 | onStatusChange: onStatusChange, 55 | onError: onError) 56 | } catch let error { 57 | onError(error as! OktaError) 58 | } 59 | } 60 | 61 | open func skipPasswordChange(onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 62 | onError: @escaping (_ error: OktaError) -> Void) { 63 | 64 | guard canSkip() else { 65 | onError(.wrongStatus("Can't find 'skip' link in response")) 66 | return 67 | } 68 | 69 | restApi.perform(link: model.links!.skip!, stateToken: stateToken) { result in 70 | 71 | self.handleServerResponse(result, 72 | onStatusChanged: onStatusChange, 73 | onError: onError) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/Statuses/OktaAuthStatePasswordResetTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | 15 | @testable import OktaAuthNative 16 | 17 | class OktaAuthStatePasswordResetTests: XCTestCase { 18 | 19 | func testResetPassword() { 20 | guard let status = createStatus() else { 21 | XCTFail() 22 | return 23 | } 24 | 25 | status.setupApiMockResponse(.SUCCESS) 26 | 27 | let ex = expectation(description: "Callback is expected!") 28 | 29 | status.resetPassword( 30 | newPassword: "1234", 31 | onStatusChange: { status in 32 | XCTAssertEqual(AuthStatus.success, status.statusType) 33 | ex.fulfill() 34 | }, 35 | onError: { error in 36 | XCTFail(error.localizedDescription) 37 | ex.fulfill() 38 | } 39 | ) 40 | 41 | waitForExpectations(timeout: 5.0) 42 | 43 | XCTAssertTrue(status.apiMock.resetPasswordCalled) 44 | } 45 | 46 | func testResetPassword_ApiFailed() { 47 | guard let status = createStatus() else { 48 | XCTFail() 49 | return 50 | } 51 | 52 | status.setupApiMockFailure() 53 | 54 | let ex = expectation(description: "Callback is expected!") 55 | 56 | status.resetPassword( 57 | newPassword: "1234", 58 | onStatusChange: { status in 59 | XCTFail("Unexpected status change!") 60 | ex.fulfill() 61 | }, 62 | onError: { error in 63 | XCTAssertEqual( 64 | "Server responded with error: Authentication failed", 65 | error.localizedDescription 66 | ) 67 | ex.fulfill() 68 | } 69 | ) 70 | 71 | waitForExpectations(timeout: 5.0) 72 | 73 | XCTAssertTrue(status.apiMock.resetPasswordCalled) 74 | } 75 | 76 | // MARK: - Utils 77 | 78 | func createStatus( 79 | from currentStatus: OktaAuthStatus = OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 80 | withResponse response: TestResponse = .PASSWORD_RESET) 81 | -> OktaAuthStatusPasswordReset? { 82 | 83 | guard let response = response.parse() else { 84 | return nil 85 | } 86 | 87 | return try? OktaAuthStatusPasswordReset(currentState: currentStatus, model: response) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/Statuses/OktaAuthStatusLockedOutTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | 15 | @testable import OktaAuthNative 16 | 17 | class OktaAuthStatusLockedOutTests: XCTestCase { 18 | 19 | func testUnlock() { 20 | guard let status = createStatus() else { 21 | XCTFail() 22 | return 23 | } 24 | 25 | status.setupApiMockResponse(.SUCCESS) 26 | 27 | let ex = expectation(description: "Callback is expected!") 28 | 29 | status.unlock( 30 | username: "test", 31 | factorType: .sms, 32 | onStatusChange: { status in 33 | XCTAssertEqual(AuthStatus.success, status.statusType) 34 | ex.fulfill() 35 | }, 36 | onError: { error in 37 | XCTFail(error.localizedDescription) 38 | ex.fulfill() 39 | } 40 | ) 41 | 42 | waitForExpectations(timeout: 5.0) 43 | 44 | XCTAssertTrue(status.apiMock.unlockCalled) 45 | } 46 | 47 | func testUnlock_ApiFailed() { 48 | guard let status = createStatus() else { 49 | XCTFail() 50 | return 51 | } 52 | 53 | status.setupApiMockFailure() 54 | 55 | let ex = expectation(description: "Callback is expected!") 56 | 57 | status.unlock( 58 | username: "test", 59 | factorType: .sms, 60 | onStatusChange: { status in 61 | XCTFail("Unexpected status changed!") 62 | ex.fulfill() 63 | }, 64 | onError: { error in 65 | XCTAssertEqual( 66 | "Server responded with error: Authentication failed", 67 | error.localizedDescription 68 | ) 69 | ex.fulfill() 70 | } 71 | ) 72 | 73 | waitForExpectations(timeout: 5.0) 74 | 75 | XCTAssertTrue(status.apiMock.unlockCalled) 76 | } 77 | 78 | // MARK: - Utils 79 | 80 | func createStatus( 81 | from currentStatus: OktaAuthStatus = OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 82 | withResponse response: TestResponse = .LOCKED_OUT) 83 | -> OktaAuthStatusLockedOut? { 84 | 85 | guard let response = response.parse() else { 86 | return nil 87 | } 88 | 89 | return try? OktaAuthStatusLockedOut(currentState: currentStatus, model: response) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusUnauthenticated.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusUnauthenticated : OktaAuthStatus { 16 | 17 | public override init(oktaDomain: URL, responseHandler: OktaAuthStatusResponseHandler = OktaAuthStatusResponseHandler()) { 18 | super.init(oktaDomain: oktaDomain, responseHandler: responseHandler) 19 | statusType = .unauthenticated 20 | } 21 | 22 | open func authenticate(username: String, 23 | password: String, 24 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 25 | onError: @escaping (_ error: OktaError) -> Void) { 26 | 27 | restApi.primaryAuthentication(username: username, 28 | password: password, 29 | deviceFingerprint: nil) 30 | { result in 31 | self.handleServerResponse(result, 32 | onStatusChanged: onStatusChange, 33 | onError: onError) 34 | } 35 | } 36 | 37 | open func unlockAccount(username: String, 38 | factorType: OktaRecoveryFactors, 39 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 40 | onError: @escaping (_ error: OktaError) -> Void) { 41 | restApi.unlockAccount(username: username, factor: factorType.toFactorType()) { result in 42 | self.handleServerResponse(result, 43 | onStatusChanged: onStatusChange, 44 | onError: onError) 45 | } 46 | } 47 | 48 | open func recoverPassword(username: String, 49 | factorType: OktaRecoveryFactors, 50 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 51 | onError: @escaping (_ error: OktaError) -> Void) { 52 | restApi.recoverPassword(username: username, factor: factorType.toFactorType()) { result in 53 | self.handleServerResponse(result, 54 | onStatusChanged: onStatusChange, 55 | onError: onError) 56 | } 57 | } 58 | 59 | override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 60 | try super.init(currentState: currentState, model: model) 61 | statusType = .unauthenticated 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/Statuses/OktaAuthStatusPasswordExpiredTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | 15 | @testable import OktaAuthNative 16 | 17 | class OktaAuthStatusPasswordExpiredTests: XCTestCase { 18 | 19 | func testChangePassword() { 20 | guard let status = createStatus() else { 21 | XCTFail() 22 | return 23 | } 24 | 25 | status.setupApiMockResponse(.SUCCESS) 26 | 27 | let ex = expectation(description: "Callback is expected!") 28 | 29 | status.changePassword( 30 | oldPassword: "1234", 31 | newPassword: "4321", 32 | onStatusChange: { status in 33 | XCTAssertEqual(AuthStatus.success, status.statusType) 34 | ex.fulfill() 35 | }, 36 | onError: { error in 37 | XCTFail(error.localizedDescription) 38 | ex.fulfill() 39 | } 40 | ) 41 | 42 | waitForExpectations(timeout: 5.0) 43 | 44 | XCTAssertTrue(status.apiMock.changePasswordCalled) 45 | } 46 | 47 | func testChangePassword_ApiFailed() { 48 | guard let status = createStatus() else { 49 | XCTFail() 50 | return 51 | } 52 | 53 | status.setupApiMockFailure() 54 | 55 | let ex = expectation(description: "Callback is expected!") 56 | 57 | status.changePassword( 58 | oldPassword: "1234", 59 | newPassword: "4321", 60 | onStatusChange: { status in 61 | XCTFail("Unexpected status change!") 62 | ex.fulfill() 63 | }, 64 | onError: { error in 65 | XCTAssertEqual( 66 | "Server responded with error: Authentication failed", 67 | error.localizedDescription 68 | ) 69 | ex.fulfill() 70 | } 71 | ) 72 | 73 | waitForExpectations(timeout: 5.0) 74 | 75 | XCTAssertTrue(status.apiMock.changePasswordCalled) 76 | } 77 | 78 | // MARK: - Utils 79 | 80 | func createStatus( 81 | from currentStatus: OktaAuthStatus = OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 82 | withResponse response: TestResponse = .PASSWORD_EXPIRED) 83 | -> OktaAuthStatusPasswordExpired? { 84 | 85 | guard let response = response.parse() else { 86 | return nil 87 | } 88 | 89 | return try? OktaAuthStatusPasswordExpired(currentState: currentStatus, model: response) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusFactorRequired.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusFactorRequired : OktaAuthStatus { 16 | 17 | public internal(set) var stateToken: String 18 | 19 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 20 | guard let stateToken = model.stateToken else { 21 | throw OktaError.invalidResponse("State token is missed") 22 | } 23 | guard let factors = model.embedded?.factors else { 24 | throw OktaError.invalidResponse("Embedded factors are missed") 25 | } 26 | self.stateToken = stateToken 27 | self.factors = factors 28 | try super.init(currentState: currentState, model: model) 29 | statusType = .MFARequired 30 | } 31 | 32 | open lazy var availableFactors: [OktaFactor] = { 33 | var oktaFactors = Array() 34 | for factor in self.factors { 35 | var createdFactor = OktaFactor.createFactorWith(factor, 36 | stateToken: stateToken, 37 | verifyLink: nil, 38 | activationLink: nil) 39 | createdFactor.restApi = restApi 40 | createdFactor.responseDelegate = self 41 | oktaFactors.append(createdFactor) 42 | } 43 | 44 | return oktaFactors 45 | }() 46 | 47 | open func selectFactor(_ factor: OktaFactor, 48 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 49 | onError: @escaping (_ error: OktaError) -> Void) { 50 | selectedFactor = factor 51 | factor.select(onStatusChange: onStatusChange, onError: onError) 52 | } 53 | 54 | override open func cancel(onSuccess: (() -> Void)? = nil, 55 | onError: ((OktaError) -> Void)? = nil) { 56 | selectedFactor?.cancel() 57 | super.cancel(onSuccess: onSuccess, onError: onError) 58 | } 59 | 60 | var factors: [EmbeddedResponse.Factor] 61 | var selectedFactor: OktaFactor? 62 | } 63 | 64 | extension OktaAuthStatusFactorRequired: OktaFactorResultProtocol { 65 | public func handleFactorServerResponse(response: OktaAPIRequest.Result, 66 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 67 | onError: @escaping (_ error: OktaError) -> Void) { 68 | self.handleServerResponse(response, onStatusChanged: onStatusChange, onError: onError) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Source/Factors/OktaFactorTotp.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaFactorTotp : OktaFactor { 16 | 17 | public var activation: EmbeddedResponse.Factor.Embedded.Activation? { 18 | get { 19 | return factor.embedded?.activation 20 | } 21 | } 22 | 23 | public var activationLinks: LinksResponse? { 24 | get { 25 | return factor.embedded?.activation?.links 26 | } 27 | } 28 | 29 | public var qrCodeLink: LinksResponse.QRCode? { 30 | get { 31 | return factor.embedded?.activation?.links?.qrcode 32 | } 33 | } 34 | 35 | public func enroll(onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 36 | onError: @escaping (_ error: OktaError) -> Void) { 37 | self.enroll(questionId: nil, 38 | answer: nil, 39 | credentialId: nil, 40 | passCode: nil, 41 | phoneNumber: nil, 42 | onStatusChange: onStatusChange, 43 | onError: onError) 44 | } 45 | 46 | public func activate(passCode: String, 47 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 48 | onError: @escaping (_ error: OktaError) -> Void) { 49 | super.activate(passCode: passCode, onStatusChange: onStatusChange, onError: onError) 50 | } 51 | 52 | public func select(passCode: String, 53 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 54 | onError: @escaping (OktaError) -> Void) { 55 | self.verifyFactor(with: links!.verify!, 56 | answer: nil, 57 | passCode: passCode, 58 | onStatusChange: onStatusChange, 59 | onError: onError) 60 | } 61 | 62 | public func verify(passCode: String, 63 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 64 | onError: @escaping (_ error: OktaError) -> Void) { 65 | super.verify(passCode: passCode, 66 | answerToSecurityQuestion: nil, 67 | onStatusChange: onStatusChange, 68 | onError: onError) 69 | } 70 | 71 | // MARK: - Internal 72 | override init(factor: EmbeddedResponse.Factor, 73 | stateToken:String, 74 | verifyLink: LinksResponse.Link?, 75 | activationLink: LinksResponse.Link?) { 76 | super.init(factor: factor, stateToken: stateToken, verifyLink: verifyLink, activationLink: activationLink) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/Factors/OktaFactorOtherTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | @testable import OktaAuthNative 15 | 16 | class OktaFactorOtherTests: OktaFactorTestCase { 17 | 18 | func testSendApiRequest() { 19 | guard let factor: OktaFactorOther = createFactor(from: .MFA_ENROLL_NotEnrolled, type: .unknown("unknown")) else { 20 | XCTFail() 21 | return 22 | } 23 | 24 | XCTAssertEqual(factor.provider, .unknown("Unknown")) 25 | 26 | factor.setupApiMockResponse(.SUCCESS) 27 | let delegate = factor.setupMockDelegate(with: try! OktaAuthStatusSuccess( 28 | currentState: OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://mock.url")!), 29 | model: TestResponse.SUCCESS.parse()! 30 | )) 31 | 32 | let ex = expectation(description: "Operation should succeed!") 33 | 34 | factor.sendRequest( 35 | with: factor.factor.links!.enroll!, 36 | keyValuePayload: [:], 37 | onStatusChange: { status in 38 | XCTAssertEqual( AuthStatus.success , status.statusType) 39 | ex.fulfill() 40 | }, 41 | onError: { error in 42 | XCTFail(error.localizedDescription) 43 | ex.fulfill() 44 | } 45 | ) 46 | 47 | waitForExpectations(timeout: 5.0) 48 | 49 | verifyDelegateSucceeded(delegate, with: .SUCCESS) 50 | 51 | XCTAssertTrue(factor.apiMock.sendApiRequestCalled) 52 | } 53 | 54 | func testSendApiRequest_ApiFailure() { 55 | guard let factor: OktaFactorOther = createFactor(from: .MFA_ENROLL_NotEnrolled, type: .unknown("unknown")) else { 56 | XCTFail() 57 | return 58 | } 59 | 60 | factor.setupApiMockFailure() 61 | let delegate = factor.setupMockDelegate(with: OktaError.internalError("Test")) 62 | 63 | let ex = expectation(description: "Operation should fail!") 64 | 65 | factor.sendRequest( 66 | with: factor.factor.links!.enroll!, 67 | keyValuePayload: [:], 68 | onStatusChange: { status in 69 | XCTFail("Operation should fail!") 70 | ex.fulfill() 71 | }, 72 | onError: { error in 73 | XCTAssertEqual(delegate.error?.localizedDescription, error.localizedDescription) 74 | ex.fulfill() 75 | } 76 | ) 77 | 78 | waitForExpectations(timeout: 5.0) 79 | 80 | verifyDelegateFailed(delegate) 81 | 82 | XCTAssertTrue(factor.apiMock.sendApiRequestCalled) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Source/OktaAuthSdk.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | public class OktaAuthSdk { 16 | 17 | public class func authenticate(with url: URL, 18 | username: String, 19 | password: String?, 20 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 21 | onError: @escaping (_ error: OktaError) -> Void) { 22 | 23 | let unauthenticatedStatus = OktaAuthStatusUnauthenticated(oktaDomain: url) 24 | unauthenticatedStatus.authenticate(username: username, 25 | password: password ?? "", 26 | onStatusChange:onStatusChange, 27 | onError:onError) 28 | } 29 | 30 | public class func unlockAccount(with url: URL, 31 | username: String, 32 | factorType: OktaRecoveryFactors, 33 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 34 | onError: @escaping (_ error: OktaError) -> Void) { 35 | 36 | let unauthenticatedStatus = OktaAuthStatusUnauthenticated(oktaDomain: url) 37 | unauthenticatedStatus.unlockAccount(username: username, 38 | factorType: factorType, 39 | onStatusChange:onStatusChange, 40 | onError: onError) 41 | } 42 | 43 | public class func recoverPassword(with url: URL, 44 | username: String, 45 | factorType: OktaRecoveryFactors, 46 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 47 | onError: @escaping (_ error: OktaError) -> Void) { 48 | 49 | let unauthenticatedStatus = OktaAuthStatusUnauthenticated(oktaDomain: url) 50 | unauthenticatedStatus.recoverPassword(username: username, 51 | factorType: factorType, 52 | onStatusChange: onStatusChange, 53 | onError: onError) 54 | } 55 | 56 | public class func fetchStatus(with stateToken: String, 57 | using url: URL, 58 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 59 | onError: @escaping (_ error: OktaError) -> Void) { 60 | let authState = OktaAuthStatus(oktaDomain: url) 61 | authState.fetchStatus(with: stateToken, onStatusChange: onStatusChange, onError: onError) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusRecovery.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusRecovery : OktaAuthStatus { 16 | 17 | public internal(set) var stateToken: String 18 | 19 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 20 | guard let stateToken = model.stateToken else { 21 | throw OktaError.invalidResponse("State token is missed") 22 | } 23 | self.stateToken = stateToken 24 | try super.init(currentState: currentState, model: model) 25 | statusType = .recovery 26 | } 27 | 28 | open var recoveryQuestion: String? { 29 | get { 30 | return model.embedded?.user?.recoveryQuestion?.question 31 | } 32 | } 33 | 34 | open var recoveryToken: String? { 35 | get { 36 | return model.recoveryToken 37 | } 38 | } 39 | 40 | open var recoveryType: OktaAPISuccessResponse.RecoveryType? { 41 | get { 42 | return model.recoveryType 43 | } 44 | } 45 | 46 | open func canRecover() -> Bool { 47 | guard model.links?.next != nil else { 48 | return false 49 | } 50 | 51 | return true 52 | } 53 | 54 | open func recoverWithAnswer(_ answer: String, 55 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 56 | onError: @escaping (_ error: OktaError) -> Void) { 57 | guard canRecover() else { 58 | onError(.wrongStatus("Can't find 'next' link in response")) 59 | return 60 | } 61 | 62 | restApi.recoverWith(answer: answer, 63 | stateToken: stateToken, 64 | recoveryToken: nil, link: model.links!.next!) { result in 65 | self.handleServerResponse(result, 66 | onStatusChanged: onStatusChange, 67 | onError: onError) 68 | } 69 | } 70 | 71 | open func recoverWithToken(_ recoveryToken: String, 72 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 73 | onError: @escaping (_ error: OktaError) -> Void) { 74 | guard canRecover() else { 75 | onError(.wrongStatus("Can't find 'next' link in response")) 76 | return 77 | } 78 | 79 | restApi.recoverWith(answer: nil, 80 | stateToken: stateToken, 81 | recoveryToken: recoveryToken, link: model.links!.next!) { result in 82 | self.handleServerResponse(result, 83 | onStatusChanged: onStatusChange, 84 | onError: onError) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Source/Factors/OktaFactorQuestion.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaFactorQuestion : OktaFactor { 16 | 17 | public var securityQuestionsLink: LinksResponse.Link? { 18 | get { 19 | return factor.links?.questions 20 | } 21 | } 22 | 23 | public var factorQuestionId: String? { 24 | get { 25 | return factor.profile?.question 26 | } 27 | } 28 | 29 | public var factorQuestionText: String? { 30 | get { 31 | return factor.profile?.questionText 32 | } 33 | } 34 | 35 | public func downloadSecurityQuestions(onDownloadComplete: @escaping ([SecurityQuestion]) -> Void, 36 | onError: @escaping (_ error: OktaError) -> Void) { 37 | guard factor.links?.questions?.href != nil else { 38 | onError(.wrongStatus("Can't find 'questions' link in response")) 39 | return 40 | } 41 | 42 | restApi?.downloadSecurityQuestions(with: factor.links!.questions!, onCompletion: onDownloadComplete, onError: onError) 43 | } 44 | 45 | public func enroll(questionId: String, 46 | answer: String, 47 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 48 | onError: @escaping (OktaError) -> Void) { 49 | self.enroll(questionId: questionId, 50 | answer: answer, 51 | credentialId: nil, 52 | passCode: nil, 53 | phoneNumber: nil, 54 | onStatusChange: onStatusChange, 55 | onError: onError) 56 | } 57 | 58 | public func select(answerToSecurityQuestion: String, 59 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 60 | onError: @escaping (OktaError) -> Void) { 61 | guard canSelect() else { 62 | onError(OktaError.wrongStatus("Can't find 'verify' link in response")) 63 | return 64 | } 65 | 66 | self.verifyFactor(with: links!.verify!, 67 | answer: answerToSecurityQuestion, 68 | passCode: nil, 69 | onStatusChange: onStatusChange, 70 | onError: onError) 71 | } 72 | 73 | public func verify(answerToSecurityQuestion: String, 74 | onStatusChange: @escaping (OktaAuthStatus) -> Void, 75 | onError: @escaping (OktaError) -> Void) { 76 | super.verify(passCode: nil, 77 | answerToSecurityQuestion: answerToSecurityQuestion, 78 | onStatusChange: onStatusChange, 79 | onError: onError) 80 | } 81 | 82 | // MARK: - Internal 83 | override init(factor: EmbeddedResponse.Factor, 84 | stateToken:String, 85 | verifyLink: LinksResponse.Link?, 86 | activationLink: LinksResponse.Link?) { 87 | super.init(factor: factor, stateToken: stateToken, verifyLink: verifyLink, activationLink: activationLink) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusResponseHandler.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusResponseHandler { 16 | 17 | public init() { 18 | } 19 | 20 | open func handleServerResponse(_ response: OktaAPIRequest.Result, 21 | currentStatus: OktaAuthStatus, 22 | onStatusChanged: @escaping (_ newState: OktaAuthStatus) -> Void, 23 | onError: @escaping (_ error: OktaError) -> Void) 24 | { 25 | var authResponse : OktaAPISuccessResponse 26 | 27 | switch response { 28 | case .error(let error): 29 | onError(error) 30 | return 31 | case .success(let success): 32 | authResponse = success 33 | } 34 | 35 | do { 36 | let status = try self.createAuthStatus(basedOn: authResponse, and: currentStatus) 37 | onStatusChanged(status) 38 | } catch let error as OktaError { 39 | onError(error) 40 | } catch { 41 | onError(OktaError.unexpectedResponse) 42 | } 43 | } 44 | 45 | open func createAuthStatus(basedOn response: OktaAPISuccessResponse, 46 | and currentStatus: OktaAuthStatus) throws -> OktaAuthStatus { 47 | guard let statusType = response.status else { 48 | throw OktaError.invalidResponse("Status is missed") 49 | } 50 | 51 | // create concrete status instance 52 | switch statusType { 53 | 54 | case .success: 55 | return try OktaAuthStatusSuccess(currentState: currentStatus, model: response) 56 | 57 | case .passwordWarning: 58 | return try OktaAuthStatusPasswordWarning(currentState: currentStatus, model: response) 59 | 60 | case .passwordExpired: 61 | return try OktaAuthStatusPasswordExpired(currentState: currentStatus, model: response) 62 | 63 | case .passwordReset: 64 | return try OktaAuthStatusPasswordReset(currentState: currentStatus, model: response) 65 | 66 | case .MFAEnroll: 67 | return try OktaAuthStatusFactorEnroll(currentState: currentStatus, model: response) 68 | 69 | case .MFAEnrollActivate: 70 | return try OktaAuthStatusFactorEnrollActivate(currentState: currentStatus, model: response) 71 | 72 | case .MFARequired: 73 | return try OktaAuthStatusFactorRequired(currentState: currentStatus, model: response) 74 | 75 | case .MFAChallenge: 76 | return try OktaAuthStatusFactorChallenge(currentState: currentStatus, model: response) 77 | 78 | case .lockedOut: 79 | return try OktaAuthStatusLockedOut(currentState: currentStatus, model: response) 80 | 81 | case .recovery: 82 | return try OktaAuthStatusRecovery(currentState: currentStatus, model: response) 83 | 84 | case .recoveryChallenge: 85 | return try OktaAuthStatusRecoveryChallenge(currentState: currentStatus, model: response) 86 | 87 | case .unauthenticated: 88 | throw OktaError.wrongStatus("Wrong state") 89 | 90 | default: 91 | throw OktaError.unknownStatus(response) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/Resources/MFA_ENROLL_PartiallyEnrolled: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"001k7sl4Ts3ZLV1yx-eC00eJbOUxL8Vfl77gAKwS_B", 3 | "expiresAt":"2019-04-18T15:35:36.000Z", 4 | "status":"MFA_ENROLL", 5 | "_embedded":{ 6 | "user":{ 7 | "id":"00ujbyedtoxuuXn9l0h7", 8 | "passwordChanged":"2019-02-11T14:09:22.000Z", 9 | "profile":{ 10 | "login":"ayurok", 11 | "firstName":"ayurok", 12 | "lastName":"ayurok", 13 | "locale":"en", 14 | "timeZone":"America/Los_Angeles" 15 | } 16 | }, 17 | "factors":[ 18 | { 19 | "factorType":"question", 20 | "provider":"OKTA", 21 | "vendorName":"OKTA", 22 | "_links":{ 23 | "questions":{ 24 | "href":"https://test.domain.com/api/v1/users/00ujbyedtoxuuXn9l0h7/factors/questions", 25 | "hints":{ 26 | "allow":[ 27 | "GET" 28 | ] 29 | } 30 | }, 31 | "enroll":{ 32 | "href":"https://test.domain.com/api/v1/authn/factors", 33 | "hints":{ 34 | "allow":[ 35 | "POST" 36 | ] 37 | } 38 | } 39 | }, 40 | "status":"NOT_SETUP", 41 | "enrollment":"REQUIRED" 42 | }, 43 | { 44 | "factorType":"sms", 45 | "provider":"OKTA", 46 | "vendorName":"OKTA", 47 | "_links":{ 48 | "enroll":{ 49 | "href":"https://test.domain.com/api/v1/authn/factors", 50 | "hints":{ 51 | "allow":[ 52 | "POST" 53 | ] 54 | } 55 | } 56 | }, 57 | "status":"ACTIVE", 58 | "enrollment":"REQUIRED", 59 | "_embedded":{ 60 | "phones":[ 61 | { 62 | "id":"mbljbyz1nyglPZm000h7", 63 | "profile":{ 64 | "phoneNumber":"+555 XX XXX 5555" 65 | }, 66 | "status":"ACTIVE" 67 | } 68 | ] 69 | } 70 | }, 71 | { 72 | "factorType":"push", 73 | "provider":"OKTA", 74 | "vendorName":"OKTA", 75 | "_links":{ 76 | "enroll":{ 77 | "href":"https://test.domain.com/api/v1/authn/factors", 78 | "hints":{ 79 | "allow":[ 80 | "POST" 81 | ] 82 | } 83 | } 84 | }, 85 | "status":"NOT_SETUP", 86 | "enrollment":"REQUIRED" 87 | }, 88 | { 89 | "factorType":"token:software:totp", 90 | "provider":"OKTA", 91 | "vendorName":"OKTA", 92 | "_links":{ 93 | "enroll":{ 94 | "href":"https://test.domain.com/api/v1/authn/factors", 95 | "hints":{ 96 | "allow":[ 97 | "POST" 98 | ] 99 | } 100 | } 101 | }, 102 | "status":"NOT_SETUP", 103 | "enrollment":"REQUIRED" 104 | } 105 | ] 106 | }, 107 | "_links":{ 108 | "cancel":{ 109 | "href":"https://test.domain.com/api/v1/authn/cancel", 110 | "hints":{ 111 | "allow":[ 112 | "POST" 113 | ] 114 | } 115 | }, 116 | "skip":{ 117 | "href":"https://test.domain.com/api/v1/authn/skip", 118 | "hints":{ 119 | "allow":[ 120 | "POST" 121 | ] 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Tests/Resources/MFA_ENROLL_NotEnrolled: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"00JILipDo2OKlc-n1oMCHuooU5MYv672uuQHMq8oVL", 3 | "expiresAt":"2019-04-18T15:28:41.000Z", 4 | "status":"MFA_ENROLL", 5 | "_embedded":{ 6 | "user":{ 7 | "id":"00ujbyedtoxuuXn9l0h7", 8 | "passwordChanged":"2019-02-11T14:09:22.000Z", 9 | "profile":{ 10 | "login":"ayurok", 11 | "firstName":"ayurok", 12 | "lastName":"ayurok", 13 | "locale":"en", 14 | "timeZone":"America/Los_Angeles" 15 | } 16 | }, 17 | "factors":[ 18 | { 19 | "factorType": "unknown", 20 | "provider": "Unknown", 21 | "_links": { 22 | "enroll": { 23 | "href": "https://lohika-um.oktapreview.com/api/v1/authn/factors", 24 | "hints": { 25 | "allow": [ 26 | "POST" 27 | ] 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "factorType": "call", 34 | "provider": "OKTA", 35 | "_links": { 36 | "enroll": { 37 | "href": "https://lohika-um.oktapreview.com/api/v1/authn/factors", 38 | "hints": { 39 | "allow": [ 40 | "POST" 41 | ] 42 | } 43 | } 44 | } 45 | }, 46 | { 47 | "factorType": "token", 48 | "provider": "RSA", 49 | "_links": { 50 | "enroll": { 51 | "href": "https://lohika-um.oktapreview.com/api/v1/authn/factors", 52 | "hints": { 53 | "allow": [ 54 | "POST" 55 | ] 56 | } 57 | } 58 | } 59 | }, 60 | { 61 | "factorType":"question", 62 | "provider":"OKTA", 63 | "vendorName":"OKTA", 64 | "_links":{ 65 | "questions":{ 66 | "href":"https://lohika-um.oktapreview.com/api/v1/users/00ujbyedtoxuuXn9l0h7/factors/questions", 67 | "hints":{ 68 | "allow":[ 69 | "GET" 70 | ] 71 | } 72 | }, 73 | "enroll":{ 74 | "href":"https://lohika-um.oktapreview.com/api/v1/authn/factors", 75 | "hints":{ 76 | "allow":[ 77 | "POST" 78 | ] 79 | } 80 | } 81 | }, 82 | "status":"NOT_SETUP", 83 | "enrollment":"REQUIRED" 84 | }, 85 | { 86 | "factorType":"sms", 87 | "provider":"OKTA", 88 | "vendorName":"OKTA", 89 | "_links":{ 90 | "enroll":{ 91 | "href":"https://lohika-um.oktapreview.com/api/v1/authn/factors", 92 | "hints":{ 93 | "allow":[ 94 | "POST" 95 | ] 96 | } 97 | } 98 | }, 99 | "status":"NOT_SETUP", 100 | "enrollment":"REQUIRED", 101 | "_embedded":{ 102 | "phones":[ 103 | { 104 | "id":"mbljbyz1nyglPZm000h7", 105 | "profile":{ 106 | "phoneNumber":"+555 XX XXX 5555" 107 | }, 108 | "status":"ACTIVE" 109 | } 110 | ] 111 | } 112 | }, 113 | { 114 | "factorType":"push", 115 | "provider":"OKTA", 116 | "vendorName":"OKTA", 117 | "_links":{ 118 | "enroll":{ 119 | "href":"https://lohika-um.oktapreview.com/api/v1/authn/factors", 120 | "hints":{ 121 | "allow":[ 122 | "POST" 123 | ] 124 | } 125 | } 126 | }, 127 | "status":"NOT_SETUP", 128 | "enrollment":"REQUIRED" 129 | }, 130 | { 131 | "factorType":"token:software:totp", 132 | "provider":"OKTA", 133 | "vendorName":"OKTA", 134 | "_links":{ 135 | "enroll":{ 136 | "href":"https://lohika-um.oktapreview.com/api/v1/authn/factors", 137 | "hints":{ 138 | "allow":[ 139 | "POST" 140 | ] 141 | } 142 | } 143 | }, 144 | "status":"NOT_SETUP", 145 | "enrollment":"REQUIRED" 146 | } 147 | ] 148 | }, 149 | "_links":{ 150 | "cancel":{ 151 | "href":"https://lohika-um.oktapreview.com/api/v1/authn/cancel", 152 | "hints":{ 153 | "allow":[ 154 | "POST" 155 | ] 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusFactorEnroll.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusFactorEnroll : OktaAuthStatus { 16 | 17 | public internal(set) var stateToken: String 18 | 19 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 20 | guard let stateToken = model.stateToken else { 21 | throw OktaError.invalidResponse("State token is missed") 22 | } 23 | guard let factors = model.embedded?.factors else { 24 | throw OktaError.invalidResponse("Embedded factors are missed") 25 | } 26 | self.stateToken = stateToken 27 | self.factors = factors 28 | try super.init(currentState: currentState, model: model) 29 | statusType = .MFAEnroll 30 | } 31 | 32 | open lazy var availableFactors: [OktaFactor] = { 33 | var oktaFactors = Array() 34 | for factor in self.factors { 35 | var createdFactor = OktaFactor.createFactorWith(factor, 36 | stateToken: stateToken, 37 | verifyLink: nil, 38 | activationLink: nil) 39 | createdFactor.restApi = restApi 40 | createdFactor.responseDelegate = self 41 | oktaFactors.append(createdFactor) 42 | } 43 | 44 | return oktaFactors 45 | }() 46 | 47 | open func canEnrollFactor(factor: OktaFactor) -> Bool { 48 | return factor.canEnroll() 49 | } 50 | 51 | open func canSkipEnrollment() -> Bool { 52 | guard model.links?.skip?.href != nil else { 53 | return false 54 | } 55 | 56 | return true 57 | } 58 | 59 | open func skipEnrollment(onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 60 | onError: @escaping (_ error: OktaError) -> Void) { 61 | guard canSkipEnrollment() else { 62 | onError(.wrongStatus("Can't find 'skip' link in response")) 63 | return 64 | } 65 | 66 | restApi.perform(link: model.links!.skip!, stateToken: stateToken) { result in 67 | self.handleServerResponse(result, 68 | onStatusChanged: onStatusChange, 69 | onError: onError) 70 | } 71 | } 72 | 73 | open func enrollFactor(factor: OktaFactor, 74 | questionId: String?, 75 | answer: String?, 76 | credentialId: String?, 77 | passCode: String?, 78 | phoneNumber: String?, 79 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 80 | onError: @escaping (_ error: OktaError) -> Void) { 81 | selectedFactor = factor 82 | factor.enroll(questionId: questionId, 83 | answer: answer, 84 | credentialId: credentialId, 85 | passCode: passCode, 86 | phoneNumber: phoneNumber, 87 | onStatusChange: onStatusChange, 88 | onError: onError) 89 | } 90 | 91 | override open func cancel(onSuccess: (() -> Void)? = nil, 92 | onError: ((OktaError) -> Void)? = nil) { 93 | selectedFactor?.cancel() 94 | super.cancel(onSuccess: onSuccess, onError: onError) 95 | } 96 | 97 | var factors: [EmbeddedResponse.Factor] 98 | var selectedFactor: OktaFactor? 99 | } 100 | 101 | extension OktaAuthStatusFactorEnroll: OktaFactorResultProtocol { 102 | public func handleFactorServerResponse(response: OktaAPIRequest.Result, 103 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 104 | onError: @escaping (_ error: OktaError) -> Void) { 105 | self.handleServerResponse(response, onStatusChanged: onStatusChange, onError: onError) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/Statuses/OktaAuthStatusRecoveryTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | @testable import OktaAuthNative 15 | 16 | class OktaAuthStatusRecoveryTests: XCTestCase { 17 | 18 | func testRecoveryChallenge() { 19 | guard let status = createStatus() else { 20 | XCTFail() 21 | return 22 | } 23 | 24 | XCTAssertNotNil(status.stateToken) 25 | XCTAssertNotNil(status.recoveryQuestion) 26 | XCTAssertNil(status.recoveryToken) 27 | XCTAssertNotNil(status.recoveryType) 28 | XCTAssert(status.canRecover()) 29 | XCTAssert(status.canCancel()) 30 | 31 | var ex = expectation(description: "Callback is expected!") 32 | status.setupApiMockResponse(.SUCCESS) 33 | status.recoverWithAnswer( 34 | "Answer", 35 | onStatusChange: { status in 36 | XCTAssertEqual(AuthStatus.success, status.statusType) 37 | ex.fulfill() 38 | }, 39 | onError: { error in 40 | XCTFail(error.localizedDescription) 41 | ex.fulfill() 42 | } 43 | ) 44 | 45 | XCTAssertTrue(status.apiMock.recoverCalled) 46 | waitForExpectations(timeout: 5.0) 47 | 48 | ex = expectation(description: "Callback is expected!") 49 | status.setupApiMockResponse(.SUCCESS) 50 | status.recoverWithToken( 51 | "Token", 52 | onStatusChange: { status in 53 | XCTAssertEqual(AuthStatus.success, status.statusType) 54 | ex.fulfill() 55 | }, 56 | onError: { error in 57 | XCTFail(error.localizedDescription) 58 | ex.fulfill() 59 | } 60 | ) 61 | 62 | XCTAssertTrue(status.apiMock.recoverCalled) 63 | waitForExpectations(timeout: 5.0) 64 | } 65 | 66 | func testChangePassword_ApiFailed() { 67 | guard let status = createStatus() else { 68 | XCTFail() 69 | return 70 | } 71 | 72 | status.setupApiMockFailure() 73 | 74 | var ex = expectation(description: "Callback is expected!") 75 | 76 | status.recoverWithAnswer( 77 | "Answer", 78 | onStatusChange: { status in 79 | XCTFail("Unexpected status change!") 80 | ex.fulfill() 81 | }, 82 | onError: { error in 83 | XCTAssertEqual( 84 | "Server responded with error: Authentication failed", 85 | error.localizedDescription 86 | ) 87 | ex.fulfill() 88 | } 89 | ) 90 | 91 | waitForExpectations(timeout: 5.0) 92 | 93 | XCTAssertTrue(status.apiMock.recoverCalled) 94 | 95 | status.setupApiMockFailure() 96 | 97 | ex = expectation(description: "Callback is expected!") 98 | 99 | status.recoverWithToken( 100 | "Token", 101 | onStatusChange: { status in 102 | XCTFail("Unexpected status change!") 103 | ex.fulfill() 104 | }, 105 | onError: { error in 106 | XCTAssertEqual( 107 | "Server responded with error: Authentication failed", 108 | error.localizedDescription 109 | ) 110 | ex.fulfill() 111 | } 112 | ) 113 | 114 | waitForExpectations(timeout: 5.0) 115 | 116 | XCTAssertTrue(status.apiMock.recoverCalled) 117 | } 118 | 119 | // MARK: - Utils 120 | 121 | func createStatus( 122 | from currentStatus: OktaAuthStatus = OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 123 | withResponse response: TestResponse = .RECOVERY) 124 | -> OktaAuthStatusRecovery? { 125 | 126 | guard let response = response.parse() else { 127 | return nil 128 | } 129 | 130 | return try? OktaAuthStatusRecovery(currentState: currentStatus, model: response) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusFactorChallenge.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusFactorChallenge : OktaAuthStatus { 16 | 17 | public internal(set) var stateToken: String 18 | 19 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 20 | guard let stateToken = model.stateToken else { 21 | throw OktaError.invalidResponse("State token is missed") 22 | } 23 | guard let factor = model.embedded?.factor else { 24 | throw OktaError.invalidResponse("Embedded factor is missed") 25 | } 26 | self.stateToken = stateToken 27 | internalFactor = factor 28 | 29 | try super.init(currentState: currentState, model: model) 30 | 31 | statusType = .MFAChallenge 32 | } 33 | 34 | open lazy var factor: OktaFactor = { 35 | var createdFactor = OktaFactor.createFactorWith(internalFactor, 36 | stateToken: stateToken, 37 | verifyLink: model.links?.next, 38 | activationLink: nil) 39 | createdFactor.responseDelegate = self 40 | createdFactor.restApi = self.restApi 41 | return createdFactor 42 | }() 43 | 44 | open func canVerify() -> Bool { 45 | return factor.canVerify() 46 | } 47 | 48 | open func canResend() -> Bool { 49 | guard model.links?.resend != nil else { 50 | return false 51 | } 52 | 53 | return true 54 | } 55 | 56 | override open func canPoll() -> Bool { 57 | return model.links?.next?.name == "poll" || factor.type == .push 58 | } 59 | 60 | open func verifyFactor(passCode: String?, 61 | answerToSecurityQuestion: String?, 62 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 63 | onError: @escaping (_ error: OktaError) -> Void) { 64 | self.factor.verify(passCode: passCode, 65 | answerToSecurityQuestion: answerToSecurityQuestion, 66 | onStatusChange: onStatusChange, 67 | onError: onError) 68 | } 69 | 70 | open func resendFactor(onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 71 | onError: @escaping (_ error: OktaError) -> Void) { 72 | guard canResend() else { 73 | onError(.wrongStatus("Can't find 'resend' link in response")) 74 | return 75 | } 76 | 77 | let link :LinksResponse.Link 78 | let resendLink = self.model.links!.resend! 79 | switch resendLink { 80 | case .resend(let rawLink): 81 | link = rawLink 82 | case .resendArray(let rawArray): 83 | link = rawArray.first! 84 | } 85 | 86 | self.restApi.perform(link: link, 87 | stateToken: stateToken, 88 | completion: { result in 89 | self.handleServerResponse(result, 90 | onStatusChanged: onStatusChange, 91 | onError: onError) 92 | }) 93 | } 94 | 95 | override open func cancel(onSuccess: (() -> Void)? = nil, 96 | onError: ((OktaError) -> Void)? = nil) { 97 | self.factor.cancel() 98 | super.cancel(onSuccess: onSuccess, onError: onError) 99 | } 100 | 101 | var internalFactor: EmbeddedResponse.Factor 102 | } 103 | 104 | extension OktaAuthStatusFactorChallenge: OktaFactorResultProtocol { 105 | public func handleFactorServerResponse(response: OktaAPIRequest.Result, 106 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 107 | onError: @escaping (_ error: OktaError) -> Void) { 108 | self.handleServerResponse(response, onStatusChanged: onStatusChange, onError: onError) 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /Tests/Statuses/OktaAuthStatusPasswordWarningTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | 15 | @testable import OktaAuthNative 16 | 17 | class OktaAuthStatusPasswordWarningTests: XCTestCase { 18 | 19 | func testChangePassword() { 20 | guard let status = createStatus() else { 21 | XCTFail() 22 | return 23 | } 24 | 25 | XCTAssertNotNil(status.model.expirationDate) 26 | XCTAssertNotNil(status.model.links?.next) 27 | XCTAssertNotNil(status.model.links?.skip) 28 | XCTAssertNotNil(status.model.links?.cancel) 29 | XCTAssertNotNil(status.model.embedded) 30 | XCTAssertNotNil(status.model.embedded?.policy) 31 | if case .password(let password)? = status.model.embedded?.policy { 32 | XCTAssertNotNil(password.complexity) 33 | XCTAssertNotNil(password.expiration) 34 | XCTAssertNotNil(password.age) 35 | } else { 36 | XCTFail("Failed to parse policy.") 37 | } 38 | 39 | status.setupApiMockResponse(.SUCCESS) 40 | 41 | let ex = expectation(description: "Callback is expected!") 42 | 43 | status.changePassword( 44 | oldPassword: "1234", 45 | newPassword: "4321", 46 | onStatusChange: { status in 47 | XCTAssertEqual(AuthStatus.success, status.statusType) 48 | ex.fulfill() 49 | }, 50 | onError: { error in 51 | XCTFail(error.localizedDescription) 52 | ex.fulfill() 53 | } 54 | ) 55 | 56 | waitForExpectations(timeout: 5.0) 57 | 58 | XCTAssertTrue(status.apiMock.changePasswordCalled) 59 | } 60 | 61 | func testChangePassword_ApiFailed() { 62 | guard let status = createStatus() else { 63 | XCTFail() 64 | return 65 | } 66 | 67 | status.setupApiMockFailure() 68 | 69 | let ex = expectation(description: "Callback is expected!") 70 | 71 | status.changePassword( 72 | oldPassword: "1234", 73 | newPassword: "4321", 74 | onStatusChange: { status in 75 | XCTFail("Unexpected status change!") 76 | ex.fulfill() 77 | }, 78 | onError: { error in 79 | XCTAssertEqual( 80 | "Server responded with error: Authentication failed", 81 | error.localizedDescription 82 | ) 83 | ex.fulfill() 84 | } 85 | ) 86 | 87 | waitForExpectations(timeout: 5.0) 88 | 89 | XCTAssertTrue(status.apiMock.changePasswordCalled) 90 | } 91 | 92 | func testSkipChangePassword() { 93 | guard let status = createStatus() else { 94 | XCTFail() 95 | return 96 | } 97 | 98 | status.setupApiMockResponse(.SUCCESS) 99 | 100 | let ex = expectation(description: "Callback is expected!") 101 | 102 | status.skipPasswordChange( 103 | onStatusChange: { status in 104 | XCTAssertEqual(AuthStatus.success, status.statusType) 105 | ex.fulfill() 106 | }, 107 | onError: { error in 108 | XCTFail(error.localizedDescription) 109 | ex.fulfill() 110 | } 111 | ) 112 | 113 | waitForExpectations(timeout: 5.0) 114 | 115 | XCTAssertTrue(status.apiMock.performCalled) 116 | } 117 | 118 | // MARK: - Utils 119 | 120 | func createStatus( 121 | from currentStatus: OktaAuthStatus = OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 122 | withResponse response: TestResponse = .PASSWORD_WARNING) 123 | -> OktaAuthStatusPasswordWarning? { 124 | 125 | guard let response = response.parse() else { 126 | return nil 127 | } 128 | 129 | return try? OktaAuthStatusPasswordWarning(currentState: currentStatus, model: response) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusFactorEnrollActivate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusFactorEnrollActivate : OktaAuthStatus { 16 | 17 | public internal(set) var stateToken: String 18 | 19 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 20 | guard let stateToken = model.stateToken else { 21 | throw OktaError.invalidResponse("State token is missed") 22 | } 23 | guard let factor = model.embedded?.factor else { 24 | throw OktaError.invalidResponse("Embedded factor is missed") 25 | } 26 | guard let activateLink = model.links?.next else { 27 | throw OktaError.invalidResponse("Links are missed") 28 | } 29 | self.stateToken = stateToken 30 | self.internalFactor = factor 31 | self.activateLink = activateLink 32 | 33 | try super.init(currentState: currentState, model: model) 34 | 35 | statusType = .MFAEnrollActivate 36 | } 37 | 38 | open lazy var factor: OktaFactor = { 39 | var createdFactor = OktaFactor.createFactorWith(internalFactor, 40 | stateToken: stateToken, 41 | verifyLink: nil, 42 | activationLink: model.links?.next) 43 | createdFactor.responseDelegate = self 44 | createdFactor.restApi = self.restApi 45 | return createdFactor 46 | }() 47 | 48 | public let activateLink: LinksResponse.Link 49 | 50 | open func canResend() -> Bool { 51 | guard model.links?.resend != nil else { 52 | return false 53 | } 54 | 55 | return true 56 | } 57 | 58 | override open func canPoll() -> Bool { 59 | return model.links?.next?.name == "poll" || factor.type == .push 60 | } 61 | 62 | open func activateFactor(passCode: String?, 63 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 64 | onError: @escaping (_ error: OktaError) -> Void) { 65 | self.factor.activate(passCode: passCode, 66 | onStatusChange: onStatusChange, 67 | onError: onError) 68 | } 69 | 70 | open func resendFactor(onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 71 | onError: @escaping (_ error: OktaError) -> Void) { 72 | guard canResend() else { 73 | onError(.wrongStatus("Can't find 'resend' link in response")) 74 | return 75 | } 76 | 77 | let link :LinksResponse.Link 78 | let resendLink = self.model.links!.resend! 79 | switch resendLink { 80 | case .resend(let rawLink): 81 | link = rawLink 82 | case .resendArray(let rawArray): 83 | link = rawArray.first! 84 | } 85 | 86 | restApi.perform(link: link, 87 | stateToken: stateToken, 88 | completion: { result in 89 | self.handleServerResponse(result, 90 | onStatusChanged: onStatusChange, 91 | onError: onError) 92 | }) 93 | } 94 | 95 | override open func cancel(onSuccess: (() -> Void)? = nil, 96 | onError: ((OktaError) -> Void)? = nil) { 97 | self.factor.cancel() 98 | super.cancel(onSuccess: onSuccess, onError: onError) 99 | } 100 | 101 | var internalFactor: EmbeddedResponse.Factor 102 | } 103 | 104 | extension OktaAuthStatusFactorEnrollActivate: OktaFactorResultProtocol { 105 | public func handleFactorServerResponse(response: OktaAPIRequest.Result, 106 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 107 | onError: @escaping (_ error: OktaError) -> Void) { 108 | self.handleServerResponse(response, onStatusChanged: onStatusChange, onError: onError) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Source/DomainObjects/AuthState.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | public enum AuthStatus { 16 | case unauthenticated 17 | case passwordWarning 18 | case passwordExpired 19 | case recovery 20 | case recoveryChallenge 21 | case passwordReset 22 | case lockedOut 23 | case MFAEnroll 24 | case MFAEnrollActivate 25 | case MFARequired 26 | case MFAChallenge 27 | case success 28 | case unknown(String) 29 | } 30 | 31 | public extension AuthStatus { 32 | init(raw: String) { 33 | switch raw { 34 | case "UNAUTHENTICATED": 35 | self = .unauthenticated 36 | case "PASSWORD_WARN": 37 | self = .passwordWarning 38 | case "PASSWORD_EXPIRED": 39 | self = .passwordExpired 40 | case "RECOVERY": 41 | self = .recovery 42 | case "RECOVERY_CHALLENGE": 43 | self = .recoveryChallenge 44 | case "PASSWORD_RESET": 45 | self = .passwordReset 46 | case "LOCKED_OUT": 47 | self = .lockedOut 48 | case "MFA_ENROLL": 49 | self = .MFAEnroll 50 | case "MFA_ENROLL_ACTIVATE": 51 | self = .MFAEnrollActivate 52 | case "MFA_REQUIRED": 53 | self = .MFARequired 54 | case "MFA_CHALLENGE": 55 | self = .MFAChallenge 56 | case "SUCCESS": 57 | self = .success 58 | default: 59 | self = .unknown(raw) 60 | } 61 | } 62 | 63 | var rawValue: String { 64 | switch self { 65 | case .unauthenticated: 66 | return "UNAUTHENTICATED" 67 | case .passwordWarning: 68 | return "PASSWORD_WARN" 69 | case .passwordExpired: 70 | return "PASSWORD_EXPIRED" 71 | case .recovery: 72 | return "RECOVERY" 73 | case .recoveryChallenge: 74 | return "RECOVERY_CHALLENGE" 75 | case .passwordReset: 76 | return "PASSWORD_RESET" 77 | case .lockedOut: 78 | return "LOCKED_OUT" 79 | case .MFAEnroll: 80 | return "MFA_ENROLL" 81 | case .MFAEnrollActivate: 82 | return "MFA_ENROLL_ACTIVATE" 83 | case .MFARequired: 84 | return "MFA_REQUIRED" 85 | case .MFAChallenge: 86 | return "MFA_CHALLENGE" 87 | case .success: 88 | return "SUCCESS" 89 | case .unknown(let raw): 90 | return raw 91 | } 92 | } 93 | 94 | @available(swift, deprecated: 1.2, obsoleted: 2.0, message: "This will be removed in v2.0. Please use rawValue instead.") 95 | var description: String { 96 | switch self { 97 | case .unauthenticated: 98 | return "Unauthenticated" 99 | case .passwordWarning: 100 | return "Password Warning" 101 | case .passwordExpired: 102 | return "Password Expired" 103 | case .recovery: 104 | return "Recovery" 105 | case .recoveryChallenge: 106 | return "Recovery Challenge" 107 | case .passwordReset: 108 | return "Password Reset" 109 | case .lockedOut: 110 | return "Locked Out" 111 | case .MFAEnroll: 112 | return "MFA Enroll" 113 | case .MFAEnrollActivate: 114 | return "MFA Enroll Activate" 115 | case .MFARequired: 116 | return "MFA Required" 117 | case .MFAChallenge: 118 | return "MFA Challenge" 119 | case .success: 120 | return "Success" 121 | case .unknown(let raw): 122 | return "Unknown (\(raw))" 123 | } 124 | } 125 | } 126 | 127 | extension AuthStatus : Equatable {} 128 | 129 | extension AuthStatus : Codable { 130 | public func encode(to encoder: Encoder) throws { 131 | var container = encoder.singleValueContainer() 132 | try container.encode(self.rawValue) 133 | } 134 | 135 | public init(from decoder: Decoder) throws { 136 | let container = try decoder.singleValueContainer() 137 | let stringValue = try container.decode(String.self) 138 | self = AuthStatus(raw: stringValue) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /OktaAuthNative.xcodeproj/xcshareddata/xcschemes/OktaAuthNative iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 77 | 78 | 82 | 83 | 87 | 88 | 92 | 93 | 97 | 98 | 99 | 100 | 106 | 107 | 113 | 114 | 115 | 116 | 118 | 119 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatusRecoveryChallenge.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatusRecoveryChallenge : OktaAuthStatus { 16 | 17 | public override init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 18 | try super.init(currentState: currentState, model: model) 19 | statusType = .recoveryChallenge 20 | } 21 | 22 | open var stateToken: String? { 23 | get { 24 | return model.stateToken 25 | } 26 | } 27 | 28 | 29 | open var recoveryType: OktaAPISuccessResponse.RecoveryType? { 30 | get { 31 | return model.recoveryType 32 | } 33 | } 34 | 35 | open var factorType: FactorType? { 36 | get { 37 | return model.factorType 38 | } 39 | } 40 | 41 | open func canVerify() -> Bool { 42 | guard model.links?.next != nil else { 43 | return false 44 | } 45 | 46 | return true 47 | } 48 | 49 | open func canResend() -> Bool { 50 | guard model.links?.resend != nil else { 51 | return false 52 | } 53 | 54 | return true 55 | } 56 | 57 | open func verifyFactor(passCode: String, 58 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 59 | onError: @escaping (_ error: OktaError) -> Void) { 60 | guard let stateToken = model.stateToken else { 61 | onError(.invalidResponse("State token is missed")) 62 | return 63 | } 64 | guard canVerify() else { 65 | onError(.wrongStatus("Can't find 'next' link in response")) 66 | return 67 | } 68 | 69 | restApi.verifyFactor(with: model.links!.next!, 70 | stateToken: stateToken, 71 | answer: nil, 72 | passCode: passCode, 73 | recoveryToken: nil, 74 | rememberDevice: nil, 75 | autoPush: nil) { result in 76 | self.handleServerResponse(result, 77 | onStatusChanged: onStatusChange, 78 | onError: onError) 79 | } 80 | } 81 | 82 | open func verifyFactor(recoveryToken: String, 83 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 84 | onError: @escaping (_ error: OktaError) -> Void) { 85 | guard let stateToken = model.stateToken else { 86 | onError(.invalidResponse("State token is missed")) 87 | return 88 | } 89 | guard canVerify() else { 90 | onError(.wrongStatus("Can't find 'next' link in response")) 91 | return 92 | } 93 | 94 | restApi.verifyFactor(with: model.links!.next!, 95 | stateToken: stateToken, 96 | answer: nil, 97 | passCode: nil, 98 | recoveryToken: recoveryToken, 99 | rememberDevice: nil, 100 | autoPush: nil) { result in 101 | self.handleServerResponse(result, 102 | onStatusChanged: onStatusChange, 103 | onError: onError) 104 | } 105 | } 106 | 107 | open func resendFactor(onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 108 | onError: @escaping (_ error: OktaError) -> Void) { 109 | guard let stateToken = model.stateToken else { 110 | onError(.invalidResponse("State token is missed")) 111 | return 112 | } 113 | guard canResend() else { 114 | onError(.wrongStatus("Can't find 'resend' link in response")) 115 | return 116 | } 117 | 118 | let link :LinksResponse.Link 119 | let resendLink = self.model.links!.resend! 120 | switch resendLink { 121 | case .resend(let rawLink): 122 | link = rawLink 123 | case .resendArray(let rawArray): 124 | link = rawArray.first! 125 | } 126 | 127 | restApi.perform(link: link, 128 | stateToken: stateToken, 129 | completion: { result in 130 | self.handleServerResponse(result, 131 | onStatusChanged: onStatusChange, 132 | onError: onError) 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Tests/Resources/MFA_REQUIRED: -------------------------------------------------------------------------------- 1 | { 2 | "stateToken":"test_state_token", 3 | "expiresAt":"2019-04-18T15:53:11.000Z", 4 | "status":"MFA_REQUIRED", 5 | "_embedded":{ 6 | "user":{ 7 | "id":"test_user_id", 8 | "passwordChanged":"2019-02-11T14:09:22.000Z", 9 | "profile":{ 10 | "login":"test_user", 11 | "firstName":"test_first_name", 12 | "lastName":"test_last_name", 13 | "locale":"en", 14 | "timeZone":"America/Los_Angeles" 15 | } 16 | }, 17 | "factors":[ 18 | { 19 | "id": "clf193zUBEROPBNZKPPE", 20 | "factorType": "call", 21 | "provider": "OKTA", 22 | "profile": { 23 | "name": "OKTA_VERIFY", 24 | "email": "some@email.com", 25 | "phoneNumber": "+1 XXX-XXX-1337" 26 | }, 27 | "_links": { 28 | "verify": { 29 | "href": "https://test.domain.com/api/v1/authn/factors/clf193zUBEROPBNZKPPE/verify", 30 | "hints": { 31 | "allow": [ 32 | "POST" 33 | ] 34 | } 35 | } 36 | } 37 | }, 38 | { 39 | "id": "rsalhpMQVYKHZKXZJQEW", 40 | "factorType": "token", 41 | "provider": "RSA", 42 | "profile": { 43 | "credentialId": "dade.murphy@example.com" 44 | }, 45 | "_links": { 46 | "verify": { 47 | "href": "https://test.domain.com/api/v1/authn/factors/rsalhpMQVYKHZKXZJQEW/verify", 48 | "hints": { 49 | "allow": [ 50 | "POST" 51 | ] 52 | } 53 | } 54 | } 55 | }, 56 | { 57 | "id":"ufskdh8bvdzPcnFQ20h7", 58 | "factorType":"question", 59 | "provider":"OKTA", 60 | "vendorName":"OKTA", 61 | "profile":{ 62 | "question":"favorite_security_question", 63 | "questionText":"What is your favorite security question?" 64 | }, 65 | "_links":{ 66 | "verify":{ 67 | "href":"https://test.domain.com/api/v1/authn/factors/ufskdh8bvdzPcnFQ20h7/verify", 68 | "hints":{ 69 | "allow":[ 70 | "POST" 71 | ] 72 | } 73 | } 74 | } 75 | }, 76 | { 77 | "id":"smskdhbk0ajTQ7ZyD0h7", 78 | "factorType":"sms", 79 | "provider":"OKTA", 80 | "vendorName":"OKTA", 81 | "profile":{ 82 | "phoneNumber":"+555 XX XXX 5555" 83 | }, 84 | "_links":{ 85 | "verify":{ 86 | "href":"https://test.domain.com/api/v1/authn/factors/smskdhbk0ajTQ7ZyD0h7/verify", 87 | "hints":{ 88 | "allow":[ 89 | "POST" 90 | ] 91 | } 92 | } 93 | } 94 | }, 95 | { 96 | "id":"opfkdh40kws5XarDb0h7", 97 | "factorType":"push", 98 | "provider":"OKTA", 99 | "vendorName":"OKTA", 100 | "profile":{ 101 | "credentialId":"test_user", 102 | "deviceType":"SmartPhone_IPhone", 103 | "keys":[ 104 | { 105 | "kty":"PKIX", 106 | "use":"sig", 107 | "kid":"default", 108 | "x5c":[ 109 | "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrmoN/w5wgjB0Rd/z2O/4SVzV/Q/wjcizvfhtbvlZuX0KYnLyoWdpdruFNAz+HEcstTVPcRIL45qivVYCp4T7D6qTqsVlSTLoe638TqiEAYPpsqRuqbe7YWc404RtAXopNVre99Uu2H6b7l+aGNpW2TAPK5KNe0YpAEV0jIJBYFPYmoxHS+e1V1qRDm/NxggMV6w4vhR3bFJZYJJSoohSKHRimeHRRPofSfdDIFA4lDpkpmn1XtWwZF9EC8pD7JdVgdtBgtgQL1adfHDjckqaKbiftPV2whekthXoFDeO9MN7Ir4vRR+oktSGS2Ei8ISX7OwtzCk527GVTIHiHeQtwIDAQAB" 110 | ] 111 | } 112 | ], 113 | "name":"Anastasia Y.", 114 | "platform":"IOS", 115 | "version":"12.2" 116 | }, 117 | "_links":{ 118 | "verify":{ 119 | "href":"https://test.domain.com/api/v1/authn/factors/opfkdh40kws5XarDb0h7/verify", 120 | "hints":{ 121 | "allow":[ 122 | "POST" 123 | ] 124 | } 125 | } 126 | } 127 | }, 128 | { 129 | "id":"ostkdh5wmlQpOvaoa0h7", 130 | "factorType":"token:software:totp", 131 | "provider":"OKTA", 132 | "vendorName":"OKTA", 133 | "profile":{ 134 | "credentialId":"test_id" 135 | }, 136 | "_links":{ 137 | "verify":{ 138 | "href":"https://test.domain.com/api/v1/authn/factors/ostkdh5wmlQpOvaoa0h7/verify", 139 | "hints":{ 140 | "allow":[ 141 | "POST" 142 | ] 143 | } 144 | } 145 | } 146 | } 147 | ], 148 | "policy":{ 149 | "allowRememberDevice":false, 150 | "rememberDeviceLifetimeInMinutes":0, 151 | "rememberDeviceByDefault":false, 152 | "factorsPolicyInfo":{ 153 | "opfkdh40kws5XarDb0h7":{ 154 | "autoPushEnabled":false 155 | } 156 | } 157 | } 158 | }, 159 | "_links":{ 160 | "cancel":{ 161 | "href":"https://test.domain.com/api/v1/authn/cancel", 162 | "hints":{ 163 | "allow":[ 164 | "POST" 165 | ] 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Example/OktaAuthNative Example.xcodeproj/xcshareddata/xcschemes/OktaAuthNative Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 66 | 67 | 71 | 72 | 76 | 77 | 78 | 79 | 80 | 81 | 91 | 93 | 99 | 100 | 101 | 102 | 103 | 104 | 110 | 112 | 118 | 119 | 120 | 121 | 123 | 124 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /Source/Statuses/OktaAuthStatus.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import Foundation 14 | 15 | open class OktaAuthStatus { 16 | 17 | public var restApi: OktaAPI 18 | 19 | public var statusType : AuthStatus = .unknown("Unknown status") 20 | 21 | public var model: OktaAPISuccessResponse 22 | 23 | public var responseHandler: OktaAuthStatusResponseHandler 24 | 25 | public init(oktaDomain: URL, 26 | responseHandler: OktaAuthStatusResponseHandler = OktaAuthStatusResponseHandler()) { 27 | self.restApi = OktaAPI(oktaDomain: oktaDomain) 28 | self.model = OktaAPISuccessResponse() 29 | self.responseHandler = responseHandler 30 | } 31 | 32 | public init(currentState: OktaAuthStatus, model: OktaAPISuccessResponse) throws { 33 | self.model = model 34 | self.restApi = currentState.restApi 35 | self.responseHandler = currentState.responseHandler 36 | } 37 | 38 | open var user: EmbeddedResponse.User? { 39 | get { 40 | return model.embedded?.user 41 | } 42 | } 43 | 44 | open var links: LinksResponse? { 45 | get { 46 | return model.links 47 | } 48 | } 49 | 50 | open var factorResult: OktaAPISuccessResponse.FactorResult? { 51 | get { 52 | return model.factorResult 53 | } 54 | } 55 | 56 | open func canReturn() -> Bool { 57 | guard model.links?.prev != nil else { 58 | return false 59 | } 60 | 61 | return true 62 | } 63 | 64 | open func canPoll() -> Bool { 65 | return false 66 | } 67 | 68 | open func returnToPreviousStatus(onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 69 | onError: @escaping (_ error: OktaError) -> Void) { 70 | guard canReturn() else { 71 | onError(.wrongStatus("Can't find 'prev' link in response")) 72 | return 73 | } 74 | 75 | guard let stateToken = model.stateToken else { 76 | onError(.invalidResponse("State token is missed")) 77 | return 78 | } 79 | 80 | restApi.perform(link: model.links!.prev!, 81 | stateToken: stateToken, 82 | completion: { result in 83 | self.handleServerResponse(result, 84 | onStatusChanged: onStatusChange, 85 | onError: onError) 86 | }) 87 | } 88 | 89 | open func canCancel() -> Bool { 90 | guard model.links?.cancel?.href != nil else { 91 | return false 92 | } 93 | 94 | return true 95 | } 96 | 97 | open func cancel(onSuccess: (() -> Void)? = nil, 98 | onError: ((_ error: OktaError) -> Void)? = nil) { 99 | 100 | guard statusType != .unauthenticated else { 101 | onSuccess?() 102 | return 103 | } 104 | guard canCancel() else { 105 | onError?(.wrongStatus("Can't find 'cancel' link in response")) 106 | return 107 | } 108 | guard let stateToken = model.stateToken else { 109 | onError?(.invalidResponse("State token is missed")) 110 | return 111 | } 112 | 113 | let completion: ((OktaAPIRequest.Result) -> Void) = { result in 114 | switch result { 115 | case .error(let error): 116 | onError?(error) 117 | case .success(_): 118 | self.cancelled = true 119 | onSuccess?() 120 | } 121 | } 122 | 123 | restApi.cancelTransaction(with: model.links!.cancel!, stateToken: stateToken, completion: completion) 124 | } 125 | 126 | // MARK: Internal 127 | internal var cancelled = false 128 | 129 | func fetchStatus(with stateToken: String, 130 | onStatusChange: @escaping (_ newStatus: OktaAuthStatus) -> Void, 131 | onError: @escaping (_ error: OktaError) -> Void) { 132 | 133 | restApi.getTransactionState(stateToken: stateToken, completion: { result in 134 | self.handleServerResponse(result, 135 | onStatusChanged: onStatusChange, 136 | onError: onError) 137 | }) 138 | } 139 | 140 | func handleServerResponse(_ response: OktaAPIRequest.Result, 141 | onStatusChanged: @escaping (_ newState: OktaAuthStatus) -> Void, 142 | onError: @escaping (_ error: OktaError) -> Void) { 143 | if cancelled { 144 | return 145 | } 146 | 147 | responseHandler.handleServerResponse(response, 148 | currentStatus: self, 149 | onStatusChanged: onStatusChanged, 150 | onError: onError) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Tests/Statuses/OktaAuthStatusFactorRequiredTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | 15 | @testable import OktaAuthNative 16 | 17 | class OktaAuthStatusFactorRequiredTests: XCTestCase { 18 | 19 | func testAvailableFactors() { 20 | guard let status = createStatus() else { 21 | XCTFail() 22 | return 23 | } 24 | 25 | let factors = status.availableFactors 26 | let expectedFactors: [FactorType] = [ 27 | .call, 28 | .token, 29 | .question, 30 | .sms, 31 | .push, 32 | .TOTP 33 | ] 34 | 35 | for (index, factor) in factors.enumerated() { 36 | XCTAssertEqual(expectedFactors[index], factor.type) 37 | XCTAssertEqual(status.stateToken, factor.stateToken) 38 | XCTAssertTrue(status === factor.responseDelegate) 39 | XCTAssertTrue(status.restApi === factor.restApi) 40 | } 41 | } 42 | 43 | // MARK: - select 44 | 45 | func testSelect() { 46 | guard let status = createStatus() else { 47 | XCTFail() 48 | return 49 | } 50 | 51 | status.setupApiMockResponse(.MFA_CHALLENGE_SMS) 52 | 53 | guard let factor = status.availableFactors.first else { 54 | XCTFail() 55 | return 56 | } 57 | 58 | let ex = expectation(description: "Callback is expected!") 59 | 60 | status.selectFactor( 61 | factor, 62 | onStatusChange: { status in 63 | XCTAssertEqual(AuthStatus.MFAChallenge, status.statusType) 64 | ex.fulfill() 65 | }, 66 | onError: { error in 67 | ex.fulfill() 68 | XCTFail(error.localizedDescription) 69 | } 70 | ) 71 | 72 | waitForExpectations(timeout: 5.0) 73 | } 74 | 75 | func testSelect_ApiFailed() { 76 | guard let status = createStatus() else { 77 | XCTFail() 78 | return 79 | } 80 | 81 | status.setupApiMockFailure() 82 | 83 | guard let factor = status.availableFactors.first else { 84 | XCTFail() 85 | return 86 | } 87 | 88 | let ex = expectation(description: "Callback is expected!") 89 | 90 | status.selectFactor( 91 | factor, 92 | onStatusChange: { status in 93 | XCTFail("Unexpected status change!") 94 | ex.fulfill() 95 | }, 96 | onError: { error in 97 | XCTAssertEqual( 98 | "Server responded with error: Authentication failed", 99 | error.localizedDescription 100 | ) 101 | ex.fulfill() 102 | } 103 | ) 104 | 105 | waitForExpectations(timeout: 5.0) 106 | } 107 | 108 | // MARK: - cancel 109 | 110 | func testCancel() { 111 | guard let status = createStatus() else { 112 | XCTFail() 113 | return 114 | } 115 | 116 | status.setupApiMockResponse(.MFA_REQUIRED) 117 | 118 | let ex = expectation(description: "Callback is expected!") 119 | 120 | XCTAssertTrue(status.canCancel()) 121 | status.cancel(onSuccess: { 122 | ex.fulfill() 123 | }, onError: { error in 124 | XCTFail(error.localizedDescription) 125 | ex.fulfill() 126 | }) 127 | 128 | waitForExpectations(timeout: 5.0) 129 | 130 | XCTAssertTrue(status.apiMock.cancelTransactionCalled) 131 | } 132 | 133 | func testCancel_ApiFailed() { 134 | guard let status = createStatus() else { 135 | XCTFail() 136 | return 137 | } 138 | 139 | status.setupApiMockFailure() 140 | 141 | let ex = expectation(description: "Callback is expected!") 142 | 143 | XCTAssertTrue(status.canCancel()) 144 | status.cancel(onSuccess: { 145 | XCTFail("Unexpected callback!") 146 | ex.fulfill() 147 | }, onError: { error in 148 | XCTAssertEqual( 149 | "Server responded with error: Authentication failed", 150 | error.localizedDescription 151 | ) 152 | ex.fulfill() 153 | }) 154 | 155 | waitForExpectations(timeout: 5.0) 156 | 157 | XCTAssertTrue(status.apiMock.cancelTransactionCalled) 158 | } 159 | 160 | // MARK: - Utils 161 | 162 | func createStatus( 163 | from currentStatus: OktaAuthStatus = OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 164 | withResponse response: TestResponse = .MFA_REQUIRED) 165 | -> OktaAuthStatusFactorRequired? { 166 | 167 | guard let response = response.parse() else { 168 | return nil 169 | } 170 | 171 | return try? OktaAuthStatusFactorRequired(currentState: currentStatus, model: response) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Tests/Statuses/OktaAuthStatusFactorEnrollActivateTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | 15 | @testable import OktaAuthNative 16 | 17 | class OktaAuthStatusFactorEnrollActivateTests: XCTestCase { 18 | 19 | func testFactor_Sms() { 20 | guard let statusSms = createStatus(withResponse: .MFA_ENROLL_ACTIVATE_SMS) else { 21 | XCTFail() 22 | return 23 | } 24 | 25 | XCTAssertEqual(FactorType.sms, statusSms.factor.type) 26 | XCTAssertTrue(statusSms === statusSms.factor.responseDelegate) 27 | XCTAssertTrue(statusSms.restApi === statusSms.factor.restApi) 28 | } 29 | 30 | func testFactor_Push() { 31 | guard let statusPush = createStatus(withResponse: .MFA_ENROLL_ACTIVATE_Push) else { 32 | XCTFail() 33 | return 34 | } 35 | 36 | XCTAssertEqual(FactorType.push, statusPush.factor.type) 37 | XCTAssertEqual(OktaAPISuccessResponse.FactorResult.waiting, statusPush.model.factorResult) 38 | XCTAssertTrue(statusPush === statusPush.factor.responseDelegate) 39 | XCTAssertTrue(statusPush.restApi === statusPush.factor.restApi) 40 | } 41 | 42 | // MARK: - resend 43 | 44 | func testResend() { 45 | guard let status = createStatus() else { 46 | XCTFail() 47 | return 48 | } 49 | 50 | status.setupApiMockResponse(.MFA_ENROLL_ACTIVATE_SMS) 51 | 52 | let ex = expectation(description: "Callback is expected!") 53 | 54 | status.resendFactor( 55 | onStatusChange: { status in 56 | XCTAssertEqual(AuthStatus.MFAEnrollActivate, status.statusType) 57 | ex.fulfill() 58 | }, 59 | onError: { error in 60 | XCTFail(error.localizedDescription) 61 | ex.fulfill() 62 | } 63 | ) 64 | 65 | waitForExpectations(timeout: 5.0) 66 | 67 | XCTAssertTrue(status.apiMock.performCalled) 68 | } 69 | 70 | func testResend_ApiFailed() { 71 | guard let status = createStatus() else { 72 | XCTFail() 73 | return 74 | } 75 | 76 | status.setupApiMockFailure() 77 | 78 | let ex = expectation(description: "Callback is expected!") 79 | 80 | status.resendFactor( 81 | onStatusChange: { status in 82 | XCTFail("Unexpected status change!") 83 | ex.fulfill() 84 | }, 85 | onError: { error in 86 | XCTAssertEqual( 87 | "Server responded with error: Authentication failed", 88 | error.localizedDescription 89 | ) 90 | ex.fulfill() 91 | } 92 | ) 93 | 94 | waitForExpectations(timeout: 5.0) 95 | 96 | XCTAssertTrue(status.apiMock.performCalled) 97 | } 98 | 99 | // MARK: - cancel 100 | 101 | func testCancel() { 102 | guard let status = createStatus() else { 103 | XCTFail() 104 | return 105 | } 106 | 107 | status.setupApiMockResponse(.MFA_ENROLL_NotEnrolled) 108 | 109 | let ex = expectation(description: "Callback is expected!") 110 | 111 | XCTAssertTrue(status.canCancel()) 112 | status.cancel(onSuccess: { 113 | ex.fulfill() 114 | }, onError: { error in 115 | XCTFail(error.localizedDescription) 116 | ex.fulfill() 117 | }) 118 | 119 | waitForExpectations(timeout: 5.0) 120 | 121 | XCTAssertTrue(status.apiMock.cancelTransactionCalled) 122 | } 123 | 124 | func testCancel_ApiFailed() { 125 | guard let status = createStatus() else { 126 | XCTFail() 127 | return 128 | } 129 | 130 | status.setupApiMockFailure() 131 | 132 | let ex = expectation(description: "Callback is expected!") 133 | 134 | XCTAssertTrue(status.canCancel()) 135 | status.cancel(onSuccess: { 136 | XCTFail("Unexpected callback!") 137 | ex.fulfill() 138 | }, onError: { error in 139 | XCTAssertEqual( 140 | "Server responded with error: Authentication failed", 141 | error.localizedDescription 142 | ) 143 | ex.fulfill() 144 | }) 145 | 146 | waitForExpectations(timeout: 5.0) 147 | 148 | XCTAssertTrue(status.apiMock.cancelTransactionCalled) 149 | } 150 | 151 | // MARK: - Utils 152 | 153 | func createStatus( 154 | from currentStatus: OktaAuthStatus = OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://test.com")!), 155 | withResponse response: TestResponse = .MFA_ENROLL_ACTIVATE_SMS) 156 | -> OktaAuthStatusFactorEnrollActivate? { 157 | 158 | guard let response = response.parse() else { 159 | return nil 160 | } 161 | 162 | return try? OktaAuthStatusFactorEnrollActivate(currentState: currentStatus, model: response) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Okta Open Source Repos 2 | ====================================== 3 | 4 | Sign the CLA 5 | ------------ 6 | 7 | If you haven't already, [sign the CLA](https://developer.okta.com/cla/). Common questions/answers are also listed on the CLA page. 8 | 9 | Summary 10 | ------- 11 | This document covers how to contribute to an Okta Open Source project. These instructions assume you have a GitHub.com account, so if you don't have one you will have to create one. Your proposed code changes will be published to your own fork of the Okta Swift SDK project and you will submit a Pull Request for your changes to be added. 12 | 13 | _Lets get started!!!_ 14 | 15 | 16 | Fork the code 17 | ------------- 18 | 19 | In your browser, navigate to: [https://github.com/okta/okta-auth-swift](https://github.com/okta/okta-auth-swift) 20 | 21 | Fork the repository by clicking on the 'Fork' button on the top right hand side. The fork will happen and you will be taken to your own fork of the repository. Copy the Git repository URL by clicking on the clipboard next to the URL on the right hand side of the page under '**HTTPS** clone URL'. You will paste this URL when doing the following `git clone` command. 22 | 23 | On your computer, follow these steps to setup a local repository for working on the Okta Swift SDK: 24 | 25 | ``` bash 26 | $ git clone https://github.com/YOUR_ACCOUNT/okta-auth-swift.git 27 | $ cd okta-auth-swift 28 | $ git remote add upstream https://github.com/okta/okta-auth-swift.git 29 | $ git checkout master 30 | $ git fetch upstream 31 | $ git rebase upstream/master 32 | ``` 33 | 34 | 35 | Making changes 36 | -------------- 37 | 38 | It is important that you create a new branch to make changes on and that you do not change the `master` branch (other than to rebase in changes from `upstream/master`). In this example I will assume you will be making your changes to a branch called `feature_x`. This `feature_x` branch will be created on your local repository and will be pushed to your forked repository on GitHub. Once this branch is on your fork you will create a Pull Request for the changes to be added to the Okta Swift SDK project. 39 | 40 | It is best practice to create a new branch each time you want to contribute to the project and only track the changes for that pull request in this branch. 41 | 42 | ``` bash 43 | $ git checkout -b feature_x 44 | (make your changes) 45 | $ git status 46 | $ git add . 47 | $ git commit -a -m "descriptive commit message for your changes" 48 | ``` 49 | 50 | > The `-b` specifies that you want to create a new branch called `feature_x`. You only specify `-b` the first time you checkout because you are creating a new branch. Once the `feature_x` branch exists, you can later switch to it with only `git checkout feature_x`. 51 | 52 | 53 | Rebase `feature_x` to include updates from `upstream/master` 54 | ------------------------------------------------------------ 55 | 56 | It is important that you maintain an up-to-date `master` branch in your local repository. This is done by rebasing in the code changes from `upstream/master` (the official Okta Swift SDK project repository) into your local repository. You will want to do this before you start working on a feature as well as right before you submit your changes as a pull request. I recommend you do this process periodically while you work to make sure you are working off the most recent project code. 57 | 58 | This process will do the following: 59 | 60 | 1. Checkout your local `master` branch 61 | 2. Synchronize your local `master` branch with the `upstream/master` so you have all the latest changes from the project 62 | 3. Rebase the latest project code into your `feature_x` branch so it is up-to-date with the upstream code 63 | 64 | ``` bash 65 | $ git checkout master 66 | $ git fetch upstream 67 | $ git rebase upstream/master 68 | $ git checkout feature_x 69 | $ git rebase master 70 | ``` 71 | 72 | > Now your `feature_x` branch is up-to-date with all the code in `upstream/master`. 73 | 74 | 75 | Make a GitHub Pull Request to contribute your changes 76 | ----------------------------------------------------- 77 | 78 | When you are happy with your changes and you are ready to contribute them, you will create a Pull Request on GitHub to do so. This is done by pushing your local changes to your forked repository (default remote name is `origin`) and then initiating a pull request on GitHub. 79 | 80 | > **IMPORTANT:** Make sure you have rebased your `feature_x` branch to include the latest code from `upstream/master` _before_ you do this. 81 | 82 | ``` bash 83 | $ git push origin master 84 | $ git push origin feature_x 85 | ``` 86 | 87 | Now that the `feature_x` branch has been pushed to your GitHub repository, you can initiate the pull request. 88 | 89 | To initiate the pull request, do the following: 90 | 91 | 1. In your browser, navigate to your forked repository: [https://github.com/YOUR_ACCOUNT/okta-auth-swift](https://github.com/YOUR_ACCOUNT/okta-auth-swift) 92 | 2. Click the new button called '**Compare & pull request**' that showed up just above the main area in your forked repository 93 | 3. Validate the pull request will be into the upstream `master` and will be from your `feature_x` branch 94 | 4. Enter a detailed description of the work you have done and then click '**Send pull request**' 95 | 96 | If you are requested to make modifications to your proposed changes, make the changes locally on your `feature_x` branch, re-push the `feature_x` branch to your fork. The existing pull request should automatically pick up the change and update accordingly. 97 | 98 | 99 | Cleaning up after a successful pull request 100 | ------------------------------------------- 101 | 102 | Once the `feature_x` branch has been committed into the `upstream/master` branch, your local `feature_x` branch and the `origin/feature_x` branch are no longer needed. If you want to make additional changes, restart the process with a new branch. 103 | 104 | > **IMPORTANT:** Make sure that your changes are in `upstream/master` before you delete your `feature_x` and `origin/feature_x` branches! 105 | 106 | You can delete these deprecated branches with the following: 107 | 108 | ``` bash 109 | $ git checkout master 110 | $ git branch -D feature_x 111 | $ git push origin :feature_x 112 | ``` 113 | -------------------------------------------------------------------------------- /Tests/Factors/OktaFactorTokenTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | import XCTest 14 | @testable import OktaAuthNative 15 | 16 | class OktaFactorTokenTests: OktaFactorTestCase { 17 | 18 | func testProperties() { 19 | guard let factor: OktaFactorToken = createFactor(from: .MFA_REQUIRED, type: .token) else { 20 | XCTFail() 21 | return 22 | } 23 | 24 | XCTAssertNotNil(factor.credentialId) 25 | XCTAssertNotNil(factor.factorProvider) 26 | XCTAssertEqual(factor.credentialId, "dade.murphy@example.com") 27 | XCTAssertEqual(factor.factorProvider, .rsa) 28 | } 29 | 30 | // MARK: - enroll 31 | 32 | func testEnroll() { 33 | guard let factor: OktaFactorToken = createFactor(from: .MFA_ENROLL_NotEnrolled, type: .token) else { 34 | XCTFail() 35 | return 36 | } 37 | 38 | factor.setupApiMockResponse(.SUCCESS) 39 | let delegate = factor.setupMockDelegate(with: try! OktaAuthStatusSuccess( 40 | currentState: OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://mock.url")!), 41 | model: TestResponse.SUCCESS.parse()! 42 | )) 43 | 44 | let ex = expectation(description: "Operation should succeed!") 45 | 46 | factor.enroll( 47 | credentialId: "dade.murphy@example.com", 48 | passCode: "1234", 49 | onStatusChange: { status in 50 | XCTAssertEqual( AuthStatus.success , status.statusType) 51 | ex.fulfill() 52 | }, 53 | onError: { error in 54 | XCTFail(error.localizedDescription) 55 | ex.fulfill() 56 | } 57 | ) 58 | 59 | waitForExpectations(timeout: 5.0) 60 | 61 | verifyDelegateSucceeded(delegate, with: .SUCCESS) 62 | XCTAssertTrue(factor.apiMock.enrollCalled) 63 | } 64 | 65 | func testEnroll_ApiFailure() { 66 | guard let factor: OktaFactorToken = createFactor(from: .MFA_ENROLL_NotEnrolled, type: .token) else { 67 | XCTFail() 68 | return 69 | } 70 | 71 | factor.setupApiMockFailure() 72 | let delegate = factor.setupMockDelegate(with: OktaError.internalError("Test")) 73 | 74 | let ex = expectation(description: "Operation should fail!") 75 | 76 | factor.enroll( 77 | credentialId: "dade.murphy@example.com", 78 | passCode: "1234", 79 | onStatusChange: { status in 80 | XCTFail("Operation should fail!") 81 | ex.fulfill() 82 | }, 83 | onError: { error in 84 | XCTAssertEqual(delegate.error?.localizedDescription, error.localizedDescription) 85 | ex.fulfill() 86 | } 87 | ) 88 | 89 | waitForExpectations(timeout: 5.0) 90 | 91 | verifyDelegateFailed(delegate) 92 | 93 | XCTAssertTrue(factor.apiMock.enrollCalled) 94 | } 95 | 96 | // MARK: - verify 97 | 98 | func testVerify() { 99 | guard let factor: OktaFactorToken = createFactor(from: .MFA_REQUIRED, type: .token) else { 100 | XCTFail() 101 | return 102 | } 103 | 104 | factor.setupApiMockResponse(.SUCCESS) 105 | let delegate = factor.setupMockDelegate(with: OktaAuthStatusUnauthenticated(oktaDomain: URL(string: "http://mock.url")!)) 106 | 107 | let ex = expectation(description: "Operation should succeed!") 108 | 109 | factor.select( 110 | passCode: "1234", 111 | onStatusChange: { status in 112 | XCTAssertEqual(delegate.changedStatus?.statusType, status.statusType) 113 | ex.fulfill() 114 | }, 115 | onError: { error in 116 | XCTFail(error.localizedDescription) 117 | ex.fulfill() 118 | } 119 | ) 120 | 121 | waitForExpectations(timeout: 5.0) 122 | 123 | verifyDelegateSucceeded(delegate, with: .SUCCESS) 124 | 125 | XCTAssertTrue(factor.apiMock.verifyFactorCalled) 126 | XCTAssertEqual("1234", factor.apiMock.factorVerificationPassCode) 127 | XCTAssertEqual(factor.verifyLink?.href, factor.apiMock.factorVerificationLink?.href) 128 | } 129 | 130 | func testVerify_ApiFailed() { 131 | guard let factor: OktaFactorToken = createFactor(from: .MFA_REQUIRED, type: .token) else { 132 | XCTFail() 133 | return 134 | } 135 | 136 | factor.setupApiMockFailure() 137 | let delegate = factor.setupMockDelegate(with: OktaError.internalError("Test")) 138 | 139 | let ex = expectation(description: "Operation should fail!") 140 | 141 | factor.select( 142 | passCode: "1234", 143 | onStatusChange: { status in 144 | XCTFail("Operation should fail!") 145 | ex.fulfill() 146 | }, 147 | onError: { error in 148 | XCTAssertEqual(delegate.error?.localizedDescription, error.localizedDescription) 149 | ex.fulfill() 150 | } 151 | ) 152 | 153 | waitForExpectations(timeout: 5.0) 154 | 155 | verifyDelegateFailed(delegate) 156 | 157 | XCTAssertTrue(factor.apiMock.verifyFactorCalled) 158 | XCTAssertEqual("1234", factor.apiMock.factorVerificationPassCode) 159 | XCTAssertEqual(factor.verifyLink?.href, factor.apiMock.factorVerificationLink?.href) 160 | } 161 | } 162 | --------------------------------------------------------------------------------