├── 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 |
--------------------------------------------------------------------------------