Skip to content

Commit

Permalink
Add support for Phone SMS and Voice as authentication factors. (#189)
Browse files Browse the repository at this point in the history
This also improves how some authenticators that require user or client intervention behave. For example, WebAuthn or OV Number Challenge now return a continuation status, which can allow the application to resume consistently.
  • Loading branch information
mikenachbaur-okta authored May 1, 2024
1 parent 768b5fe commit d0ff813
Show file tree
Hide file tree
Showing 25 changed files with 565 additions and 187 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/documentation-ghpages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- master

env:
DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer
NSUnbufferedIO: YES

# NOTE: The DocC `generate-documentation` plugin does not handle the
Expand All @@ -20,7 +20,7 @@ env:
jobs:
ExportToGHPages:
name: Export to Github Pages
runs-on: macos-12
runs-on: macos-latest-large
steps:
- uses: actions/checkout@master
- name: Build Documentation
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ on:
- 'Sources/**/*.md'

env:
DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer
NSUnbufferedIO: YES

jobs:
BuildDocumentation:
name: Build Documentation Archives
runs-on: macos-12
runs-on: macos-latest-large
steps:
- uses: actions/checkout@master
- name: AuthFoundation
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ on:

jobs:
SwiftLint:
runs-on: macos-latest
runs-on: macos-latest-large
steps:
- uses: actions/checkout@v1
- name: Lint code using SwiftLint
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ on:
- 'Tests/**/*.swift'

env:
DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer
NSUnbufferedIO: YES
iOS_DESTINATION: "platform=iOS Simulator,OS=16.2,name=iPhone 14 Pro Max"
tvOS_DESTINATION: "platform=tvOS Simulator,OS=16.1,name=Apple TV"
iOS_DESTINATION: "platform=iOS Simulator,OS=17.4,name=iPhone 15 Pro Max"
tvOS_DESTINATION: "platform=tvOS Simulator,OS=17.4,name=Apple TV"

jobs:
SwiftBuild:
name: Swift Unit Tests
runs-on: macos-12
runs-on: macos-latest-large
timeout-minutes: 10
steps:
- name: Get swift version
Expand All @@ -42,7 +42,7 @@ jobs:

Cocoapods:
name: CocoaPods Build
runs-on: macos-12
runs-on: macos-latest-large
timeout-minutes: 10
needs:
- SwiftBuild
Expand All @@ -60,7 +60,7 @@ jobs:

XcodeBuild:
name: Xcode Unit Tests
runs-on: macos-12
runs-on: macos-latest-large
timeout-minutes: 25
steps:
- uses: actions/checkout@master
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
967D0ACE29F89379002A5AD3 /* SignInScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967D0ACC29F89231002A5AD3 /* SignInScreen.swift */; };
967D0AD129FAE193002A5AD3 /* SwiftOTP in Frameworks */ = {isa = PBXBuildFile; productRef = 967D0AD029FAE193002A5AD3 /* SwiftOTP */; };
967D0AD329FAE48E002A5AD3 /* DirectAuth2FASignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967D0AD229FAE48E002A5AD3 /* DirectAuth2FASignInTests.swift */; };
E05F32392BE0736A00BB20D1 /* ContinuationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E05F32382BE0736A00BB20D1 /* ContinuationView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -72,6 +73,7 @@
967D0AC929EF47F3002A5AD3 /* SecondaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryView.swift; sourceTree = "<group>"; };
967D0ACC29F89231002A5AD3 /* SignInScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInScreen.swift; sourceTree = "<group>"; };
967D0AD229FAE48E002A5AD3 /* DirectAuth2FASignInTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectAuth2FASignInTests.swift; sourceTree = "<group>"; };
E05F32382BE0736A00BB20D1 /* ContinuationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinuationView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -126,6 +128,7 @@
967D0ABE29EDB274002A5AD3 /* SignInView.swift */,
967D0AC729EF47EB002A5AD3 /* PrimaryView.swift */,
967D0AC929EF47F3002A5AD3 /* SecondaryView.swift */,
E05F32382BE0736A00BB20D1 /* ContinuationView.swift */,
967D0AC229EE1E03002A5AD3 /* UnconfiguredView.swift */,
967D0A3229EA1BE4002A5AD3 /* Main.storyboard */,
967D0A3529EA1BE6002A5AD3 /* Assets.xcassets */,
Expand Down Expand Up @@ -325,6 +328,7 @@
967D0A9A29EA1CAD002A5AD3 /* TokenDetailViewController.swift in Sources */,
967D0ABF29EDB274002A5AD3 /* SignInView.swift in Sources */,
967D0A3129EA1BE4002A5AD3 /* SignInViewController.swift in Sources */,
E05F32392BE0736A00BB20D1 /* ContinuationView.swift in Sources */,
967D0AC829EF47EB002A5AD3 /* PrimaryView.swift in Sources */,
967D0A2D29EA1BE4002A5AD3 /* AppDelegate.swift in Sources */,
967D0A2F29EA1BE4002A5AD3 /* SceneDelegate.swift in Sources */,
Expand Down
84 changes: 84 additions & 0 deletions Samples/DirectAuthSignIn/DirectAuthSignIn/ContinuationView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
//
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and limitations under the License.
//

import SwiftUI
import OktaDirectAuth

extension SignInView {
struct ContinuationView: View {
let flow: DirectAuthenticationFlow

@State var status: DirectAuthenticationFlow.Status
@State var selectedFactor: SignInView.Factor = .code
@State var verificationCode: String = ""

var factor: DirectAuthenticationFlow.ContinuationFactor? {
switch selectedFactor {
case .code:
return .prompt(code: verificationCode)
default:
return nil
}
}

@Binding var error: Error?
@Binding var hasError: Bool

var body: some View {
VStack {
Text("Please continue authenticating.")
.padding(25)

VStack(alignment: .leading, spacing: 1) {
Picker(selection: $selectedFactor, label: EmptyView()) {
ForEach(SignInView.Factor.continuationFactors, id: \.self) {
Text($0.title)
}
}.pickerStyle(.menu)
.accessibilityIdentifier("factor_type_button")
.padding(.horizontal, -10)
.padding(.vertical, -4)

if selectedFactor == .code {
TextField("123456", text: $verificationCode)
.textContentType(.oneTimeCode)
.accessibilityIdentifier("verification_code_button")
.padding(10)
.overlay {
RoundedRectangle(cornerRadius: 6)
.stroke(.secondary, lineWidth: 1)
}
}

if let factor = factor {
Button("Continue") {
Task {
do {
status = try await flow.resume(status, with: factor)
if case let .success(token) = status {
Credential.default = try Credential.store(token)
}
} catch {
self.error = error
self.hasError = true
}
}
}
.accessibilityIdentifier("signin_button")
.font(.headline)
.buttonStyle(.borderedProminent)
}
}.padding()
}
}
}
}
36 changes: 22 additions & 14 deletions Samples/DirectAuthSignIn/DirectAuthSignIn/PrimaryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,20 @@ extension SignInView {
@State var oneTimeCode: String = ""
@State var selectedFactor: SignInView.Factor = .password

var factor: DirectAuthenticationFlow.PrimaryFactor {
var factor: DirectAuthenticationFlow.PrimaryFactor? {
switch selectedFactor {
case .password:
return .password(password)
case .otp:
return .otp(code: oneTimeCode)
case .oob:
return .oob(channel: .push)
case .sms:
return .oob(channel: .sms)
case .voice:
return .oob(channel: .voice)
default:
return nil
}
}

Expand Down Expand Up @@ -86,26 +92,28 @@ extension SignInView {
RoundedRectangle(cornerRadius: 6)
.stroke(.secondary, lineWidth: 1)
}
case .oob: EmptyView()
case .oob, .sms, .voice, .code: EmptyView()
}
}

Button("Sign In") {
Task {
do {
status = try await flow.start(username, with: factor)
if case let .success(token) = status {
Credential.default = try Credential.store(token)
if let factor = factor {
Button("Sign In") {
Task {
do {
status = try await flow.start(username, with: factor)
if case let .success(token) = status {
Credential.default = try Credential.store(token)
}
} catch {
self.error = error
self.hasError = true
}
} catch {
self.error = error
self.hasError = true
}
}
.accessibilityIdentifier("signin_button")
.font(.headline)
.buttonStyle(.borderedProminent)
}
.accessibilityIdentifier("signin_button")
.font(.headline)
.buttonStyle(.borderedProminent)
}.padding()
}
}
Expand Down
6 changes: 6 additions & 0 deletions Samples/DirectAuthSignIn/DirectAuthSignIn/SecondaryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ extension SignInView {
return .oob(channel: .push)
case .password:
return nil
case .sms:
return .oob(channel: .sms)
case .voice:
return .oob(channel: .voice)
default:
return nil
}
}

Expand Down
32 changes: 23 additions & 9 deletions Samples/DirectAuthSignIn/DirectAuthSignIn/SignInView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,50 @@ struct SignInView: View {
@State var hasError: Bool = false

enum Factor {
case password, otp, oob
case password, otp, oob, sms, voice, code

var title: String {
switch self {
case .password: return "Password"
case .otp: return "One-Time Code"
case .oob: return "Push Notification"
case .sms: return "Phone: SMS"
case .voice: return "Phone: Voice"
case .code: return "Verification Code"
}
}

static let primaryFactors: [Factor] = [.password, .otp, .oob]
static let secondaryFactors: [Factor] = [.otp, .oob]
static let primaryFactors: [Factor] = [.password, .otp, .oob, .sms, .voice]
static let secondaryFactors: [Factor] = [.otp, .oob, .sms, .voice]
static let continuationFactors: [Factor] = [.code]
}

// swiftlint:disable force_unwrapping
var body: some View {
VStack {
VStack(alignment: .leading, spacing: 15) {
Text("Direct Authentication Sign In")
.font(.title)

if let flow = flow {
if let status = status {
SecondaryView(flow: flow,
status: status,
error: $error,
hasError: $hasError)
} else {
switch status {
case nil:
PrimaryView(flow: flow,
status: $status,
error: $error,
hasError: $hasError)
case .mfaRequired(_):
SecondaryView(flow: flow,
status: status!,
error: $error,
hasError: $hasError)
case .continuation(_):
ContinuationView(flow: flow,
status: status!,
error: $error,
hasError: $hasError)
case .success(_):
ProgressView()
}
} else {
UnconfiguredView()
Expand Down Expand Up @@ -80,6 +93,7 @@ struct SignInView: View {
}
.navigationTitle("Direct Authentication")
}
// swiftlint:enable force_unwrapping
}

// swiftlint:disable force_unwrapping
Expand Down
Loading

0 comments on commit d0ff813

Please sign in to comment.