# Authenticating with a web service

> Using ASWebAuthenticationSession and AuthenticationServices to authenticate a SwiftUI app against a web service, safely.

Source: https://delightfulapps.co/blog/authenticating-with-a-web-service/
Published: 2020-03-09

Knowing who you are is a difficult thing at the best of times. Allowing an app to authenticate against a web service is likewise as difficult due to the level of trust that needs to be created between you and the system handling the authentication. Thankfully Apple has a solution to the authentication problem known as [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) that exists as part of the [AuthenticationServices](https://developer.apple.com/documentation/authenticationservices) framework.

So what makes authentication a difficult? Can't we just send username and password in the clear? The answer to that is simple. No, we can't. We need to make sure that who we are authenticating against is who we expect. We need to make sure that we create the trust between our app and the web service. Let's look at how this is done.

The first component we want is `ASWebAuthenticationSession`, though in order to make use of that we need to build a few things first. By doing this, we are able to expose parts of AppKit or UIKit to SwiftUI so that `ASWebAuthenticationSession` does it thing. So what does it require? It requires a `ASPresentationAnchor`. This typically is a window being either an `NSWindow` or an `UIWindow`. In the land of SwiftUI, this is done through an environment variable. Let's build this out.

```swift
struct WindowKey: EnvironmentKey {
    static let defaultValue: NSWindow? = nil
}

extension EnvironmentValues {
    var window: NSWindow? {
        get {
            self[WindowKey.self]
        }
        set {
            self[WindowKey.self] = newValue
        }
    }
}
```

So what exactly does that do? It provides our SwiftUI views with the knowledge of the window and we'll use that later on. We'll move on and start building out the app parts. First up is the view that will allow us to present everything. As you guessed, it's a button that we tap.

```swift
struct AuthView: View {
  @Environment(\.window) var window: NSWindow?
  let store: AuthStore

  var body: some View {
    VStack(alignment: .center, spacing: 16.0) {
      Button(action: {
        self.store.auth()
      }) {
        Text("Login with GitHub")
          .font(.headline)
      }
    }
  }
}
```

There are a few bits we need to fill out here, the first of which is our `AuthStore`. Lets look at this and how it's pieced together.

```swift
final class AuthStore {
  private var helper: AuthHelper?
  private var authCancellable: AnyCancellable? {
    willSet {
      authCancellable?.cancel()
    }
  }

  init(window: NSWindow?) {
    helper = AuthHelper(window: window)
  }

  func auth() {
    authCancellable = helper?.authenticate()
      .sink(receiveCompletion: { completion in

      }, receiveValue: { apiKey in
        if AuthKeychainHelper.tokenExists {
          try? AuthKeychainHelper.update(token: apiKey)
        } else {
          try? AuthKeychainHelper.store(token: apiKey)
        }
      })
  }
}
```

This involves one piece that we wont touch on being the Keychain helper. Suffice to say, it's for storing the resulting api token. The piece of interest though is the `AuthHelper` which handles our interaction with `ASWebAuthenticationSession`. The full implementation will be placed at the end, but for now we'll break apart the interesting pieces.

To begin, we have the type that conforms to `ASWebAuthenticationPresentationContextProviding` and tells us how the web auth window should be presented. When dealing with `ASWebAuthenticationSession` we want to be providing it with an instance of this type.

```swift
class AuthServiceContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
  let window: NSWindow

  init(window: NSWindow) {
    self.window = window
  }

  func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
    return window
  }
}
```

The second part is our call to `ASWebAuthenticationSession`. We are wrapping this in a Future so that it's exposed to SwiftUI using Combine.

```swift
  func authenticate() -> AnyPublisher<String, Errors> {
    let scopes = [
      "user",
      "public_repo",
      "repo",
      "repo:status",
      "read:org"
    ]

    var urlComponents = URLComponents(string: "")
    urlComponents?.queryItems = [
      URLQueryItem(name: "client_id", value: "abc123"),
      URLQueryItem(name: "scope", value: scopes.joined(separator: " "))
    ]

    guard let url = urlComponents?.url else {
      return Fail(error: Errors.invalidURL).eraseToAnyPublisher()
    }

    return Future<String, Errors> { promise in
      let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "appscheme") { (url, error) in
        if let error = error as? ASWebAuthenticationSessionError {
          switch error.code {
          case .canceledLogin:
            promise(.failure(.userCancelled))
          default:
            promise(.failure(.unknown))
          }
        }

        if let url = url, let oauthToken = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.filter({ $0.name == "code" }).first?.value {
          promise(.success(oauthToken))
        } else {
          promise(.failure(.noToken))
        }
      }

      session.presentationContextProvider = self.contextProvider
      session.start()
    }
    .flatMap(fetchToken(with:))
    .receive(on: RunLoop.main)
    .eraseToAnyPublisher()
  }
```

Here we have asked `ASWebAuthenticationSession` to authenticate against the given URL (in my projects case, GitHub). The second interesting part is what we do with the token we get back. Unfortunately what GitHub returns us isn't the final api token we use for requests. We need to make another call to get that. This happens in the `fetchToken(with:)` function. Let's unpack that now.

There's a curiosity here in the code. What exactly is the `callbackURLScheme`? This is how the service knows to call back into your app once you have been authenticated. You'll need to set this up in your info.plist. The scheme you define there is the one you provide here.

```swift
  private func fetchToken(with token: String) -> AnyPublisher<String, Errors> {
    var components = URLComponents(string: "")
    let queryItems: [URLQueryItem] = [
      URLQueryItem(name: "client_id", value: "123abc"),
      URLQueryItem(name: "client_secret", value: "abc123"),
      URLQueryItem(name: "code", value: token)
    ]

    components?.queryItems = queryItems

    guard let url = components?.url else {
      return Fail(error: .invalidURL).eraseToAnyPublisher()
    }

    var request = URLRequest(url: url)
    request.addValue("application/json", forHTTPHeaderField: "Accept")
    request.httpMethod = "POST"

    return URLSession.shared
      .dataTaskPublisher(for: request)
      .map { $0.data }
      .decode(type: Response.self, decoder: decoder)
      .mapError { error in
        if let error = error as? Errors {
          return error
        }

        return Errors.jsonDecoding
      }
      .map { $0.accessToken }
      .eraseToAnyPublisher()
  }
```

The result of this is our api token after having been authenticated with the web service.

As mentioned, the entire implementation of the auth helper is included.

```swift
class AuthHelper {
  struct Response: Decodable {
    let accessToken: String
    let scope: String
    let tokenType: String

    enum CodingKeys: String, CodingKey {
      case accessToken = "access_token"
      case scope
      case tokenType = "token_type"
    }
  }

  enum Errors: Error {
    case invalidURL
    case userCancelled
    case noToken
    case unknown
    case jsonDecoding
  }

  private var contextProvider: AuthServiceContextProvider
  private var decoder: JSONDecoder

  init?(window: NSWindow?, environment: AppEnvironment) {
    guard let window = window else { return nil }

    contextProvider = AuthServiceContextProvider(
      window: window
    )
    self.decoder = JSONDecoder()
  }

  func authenticate() -> AnyPublisher<String, Errors> {
    let scopes = [
      "user",
      "public_repo",
      "repo",
      "repo:status",
      "read:org"
    ]

    var urlComponents = URLComponents(string: "")
    urlComponents?.queryItems = [
      URLQueryItem(name: "client_id", value: ""),
      URLQueryItem(name: "scope", value: scopes.joined(separator: " "))
    ]

    guard let url = urlComponents?.url else {
      return Fail(error: Errors.invalidURL).eraseToAnyPublisher()
    }

    return Future<String, Errors> { promise in
      let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "") { (url, error) in
        if let error = error as? ASWebAuthenticationSessionError {
          switch error.code {
          case .canceledLogin:
            promise(.failure(.userCancelled))
          default:
            promise(.failure(.unknown))
          }
        }

        if let url = url, let oauthToken = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.filter({ $0.name == "code" }).first?.value {
          promise(.success(oauthToken))
        } else {
          promise(.failure(.noToken))
        }
      }

      session.presentationContextProvider = self.contextProvider
      session.start()
    }
    .flatMap(fetchToken(with:))
    .receive(on: RunLoop.main)
    .eraseToAnyPublisher()
  }

  private func fetchToken(with token: String) -> AnyPublisher<String, Errors> {
    var components = URLComponents(string: "")
    let queryItems: [URLQueryItem] = [
      URLQueryItem(name: "client_id", value: ""),
      URLQueryItem(name: "client_secret", value: ""),
      URLQueryItem(name: "code", value: token)
    ]

    components?.queryItems = queryItems

    guard let url = components?.url else {
      return Fail(error: .invalidURL).eraseToAnyPublisher()
    }

    var request = URLRequest(url: url)
    request.addValue("application/json", forHTTPHeaderField: "Accept")
    request.httpMethod = "POST"

    return URLSession.shared
      .dataTaskPublisher(for: request)
      .map { $0.data }
      .decode(type: Response.self, decoder: decoder)
      .mapError { error in
        if let error = error as? Errors {
          return error
        }

        return Errors.jsonDecoding
      }
      .map { $0.accessToken }
      .eraseToAnyPublisher()
  }
}
```

I hope this has been an insightful article. One that allows you to build some amazing products. If you are after help in such amazing applications, please do get in touch using the links below.

---

Delightful Apps — https://delightfulapps.co · amy@delightfulapps.co
