Two Factor Authentication with Network Extension

Is there any way to achieve 2FA with Network Extension? I'm getting a challenge from the server on initial authentication, after which I need to show a screen to get the OTP from user. But how to keep the tunnel in waiting/deferred state and also update the state to UI layer where I can ask the user for OTP?

Thanks in advance!

Answered by DTS Engineer in 724221022

But, is there any way to show UI before returning from startTunnel(options:)?

There is no way to show UI directly from your tunnel provider. I mentioned that up front.

The question of “before returning” is a subtle one. Prior to the advent of Swift concurrency the answer is clearly “No.” That’s because interacting with the user is an async process and you can’t block inside startTunnel(options:completionHandler:) waiting for that process to complete.

Having said that, it’s not normally a problem because returning from startTunnel(options:completionHandler:) is pretty much irrelevant. The system doesn’t consider the start operation done until you call the supplied completion handling.

If you take advantage of Swift concurrency then this whole completion handler stuff goes away. In that model you’re allowed to wait for async functions inside startTunnel(options:), and interacting with the user is one such operation.


Coming back to the user interaction issue, the standard approach here is for your tunnel provider to post a local notification requesting that the user run your container app. That app can present UI, including the two-factor authentication UI. On receiving that notification the app:

  1. Checks in with the provider to see what authentication requests are pending.

  2. Displays those to the user.

  3. Sends the results back to the provider.

To run the IPC with the provider, use the app messaging system. In the app, call the sendProviderMessage(_:responseHandler:) method to send messages to the provider. In the provider, override the handleAppMessage(_:completionHandler:) method to learn about these messages from the app.

To notify the user, use the User Notifications framework.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

There are two parts to this:

  • Delaying the start of the tunnel while you wait for the user to respond.

  • Actually interacting with the user.


The first is straightforward. The startTunnel(options:) method is async [1]. The requirement is that you call setTunnelNetworkSettings(_:) before returning, but that’s it. If you need to do some extended task, you can do so. In a typical VPN that extended task involves opening a connection to the VPN server, negotiating options, and so on, but there’s no reason it couldn’t involve waiting for user interaction.

Consider this:

override func startTunnel(options: [String : NSObject]?) async throws {
    let settings: NETunnelNetworkSettings
    … take as much time as you want to set up `settings` here …
    try await self.setTunnelNetworkSettings(settings)
}

The second issue in trickier. The correct approach depends on the context, with the one constant being that your NE provider is not allowed to present UI. So:

  • What platform are you targeting?

  • If it’s macOS, are you building an appex or a sysex?

  • What type of provider are you building? A packet tunnel? An app proxy? Or something else?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] This assumes you’re working on a packet tunnel but the story is very similar for an app proxy.

Hi Eskimo,

Thanks for your quick response. But I'm trying to hold the tunnel after starting it. Basically what happens is I start the tunnel, it tries to authenticate the server but a challenge is returned from server asking for OTP, this is the point at which I want the tunnel/session to wait for re-authentication.

FYI, I'm working with Packet Tunnel and my target platform is iOS.

Regards, Ganaraj Savant

But I'm trying to hold the tunnel after starting it.

OK, I need to clarify that. Do you get this challenge during the initial tunnel bring up? Or have you brought up the tunnel already and then, while the tunnel is already operating, you get a challenge?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Yeah, I get the challenge during the initial tunnel bring up. That is after TLS handshake, where we usually get the "AUTH_FAILED" message.

Yeah, I get the challenge during the initial tunnel bring up.

OK, so then I’m confused by your earlier comment. In the structure I outlined earlier, your startTunnel(…) function is asynchronous and the tunnel is only considered ‘up’ when you return from it. You can take as much time as you like opening the TLS connection to your VPN server and then, if you receive a challenge, presenting that challenge to the user. When all of that is done, call setTunnelNetworkSettings(…) and then return to indicate that the tunnel is now available for folks to use.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Okay, got it, my bad. But, is there any way to show UI before returning from startTunnel(options:)?

Accepted Answer

But, is there any way to show UI before returning from startTunnel(options:)?

There is no way to show UI directly from your tunnel provider. I mentioned that up front.

The question of “before returning” is a subtle one. Prior to the advent of Swift concurrency the answer is clearly “No.” That’s because interacting with the user is an async process and you can’t block inside startTunnel(options:completionHandler:) waiting for that process to complete.

Having said that, it’s not normally a problem because returning from startTunnel(options:completionHandler:) is pretty much irrelevant. The system doesn’t consider the start operation done until you call the supplied completion handling.

If you take advantage of Swift concurrency then this whole completion handler stuff goes away. In that model you’re allowed to wait for async functions inside startTunnel(options:), and interacting with the user is one such operation.


Coming back to the user interaction issue, the standard approach here is for your tunnel provider to post a local notification requesting that the user run your container app. That app can present UI, including the two-factor authentication UI. On receiving that notification the app:

  1. Checks in with the provider to see what authentication requests are pending.

  2. Displays those to the user.

  3. Sends the results back to the provider.

To run the IPC with the provider, use the app messaging system. In the app, call the sendProviderMessage(_:responseHandler:) method to send messages to the provider. In the provider, override the handleAppMessage(_:completionHandler:) method to learn about these messages from the app.

To notify the user, use the User Notifications framework.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi Eskimo,

Thanks for your constant support, I'm almost there.

Everything seems to be working fine till handleAppMessage(_:completionHandler:) method to learn about the messages from the app. Actually, I get updated credentials from the app. Now, how do I update it to the server so that it re-authenticates and a connection is established?

Actually, I get updated credentials from the app.

Yay!

Now, how do I update it to the server so that it re-authenticates and a connection is established?

That’s kinda between you and your server. You’re in charge of the network connection between your tunnel provider and your server, so the details of how you get it to re-authenticate are specific to the protocol you’re using.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Got it, thanks!

Two Factor Authentication with Network Extension
 
 
Q