# PKCE OAuth and the Plugin SDK for macOS

<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

Authorization Code with Proof Key for Code Exchange (PKCE) is an OAuth flow similar to the standard
Authorization Code flow, except it doesn't require that you have a backend server to get the authorization token.

## Prerequisites

-   Understanding of Swift.
-   Zoom Plugin SDK for macOS.
-   [OAuth credentials](/docs/integrations/oauth/) for a Marketplace app with the scopes your plugin needs.
-   An authorization URL from your Marketplace OAuth app. This is the URL users are directed to for OAuth.

> **NOTE** Throughout this guide, credentials are hardcoded for convenience. For security reasons, do not store hardcoded credentials of any type in your production application.

After you've created your project, downloaded the Plugin SDK files, integrated the SDK into your app, and
gotten the authorization URL from your Marketplace OAuth app, you can authenticate users with PKCE and use
the returned access token to call the Zoom REST APIs your plugin's scopes authorize.

## Authenticate users using PKCE

Now that your project is set up, let's walk through the steps of implementing the PKCE OAuth flow.

### Generate the code verifier and challenge

First, generate a code verifier using the next code sample, then hash the verifier to create a code
challenge. Host these in a dedicated `CodeChallengeHelper` class. Then define the class with a verifier
`var` in Swift, since the same verifier must be used for the auth session and requesting the access token.

```swift
import Foundation
import CommonCrypto

class CodeChallengeHelper {
    var verifier: String? = nil
}
Within that same class, define two methods: createCodeVerifier to generate a new verifier and getCodeChallenge to create a code challenge using the verifier.
func createCodeVerifier() {
    var buffer = [UInt8](repeating: 0, count: 32)
    _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
    verifier = Data(buffer).base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")
        .trimmingCharacters(in: .whitespaces)
}
func getCodeChallenge() -> String {
    guard let verifier = verifier,
    let data = verifier.data(using: .ascii) else { return "" }
    var buffer = [UInt8](repeating: 0,  count: Int(CC_SHA256_DIGEST_LENGTH))
    data.withUnsafeBytes {
        _ = CC_SHA256($0, CC_LONG(data.count), &buffer)
    }
    let hash = Data(buffer)
    let challenge = hash.base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")
        .trimmingCharacters(in: .whitespaces)
    return challenge
}
```

### Start authentication session

After creating the challenge, use it to create the authorization URL and pass that URL into an
`ASWebAuthenticationSession` which will help manage the authentication. For the sake of this guide,
we won't go into detail about what this service does. Apple's documentation provides more information
for those who are interested.

First, set up the `ViewController` in your project to conform to `ASWebAuthenticationPresentationContextProviding`
and create a couple of constants for later use.

```swift
import Cocoa
import AuthenticationServices

class ViewController: NSViewController {
    private let codeChallengeHelper = CodeChallengeHelper()
    private let delegate = NSApplication.shared.delegate as! AppDelegate
```

Next, create a starting point to kick off the actual PKCE logic. Since we're using `ASWebAuthenticationSession`,
you must call the code below after the `viewDidAppear` callback. Otherwise, your app will not be able to
start the session.

Make note of the `TODO` lines, which require you to input your client ID and redirect URI from your Marketplace
OAuth app. As a reminder, do not hardcode credentials in a production environment, as this puts those
credentials in reach of any bad actors who try to hack your app.

```swift
codeChallengeHelper.createCodeVerifier()
guard var oauthUrlComp = URLComponents(string: "https://zoom.us/oauth/authorize") else { return }

let codeChallenge = URLQueryItem(name: "code_challenge", value: codeChallengeHelper.getCodeChallenge())
let codeChallengeMethod = URLQueryItem(name: "code_challenge_method", value: "S256")
let responseType = URLQueryItem(name: "response_type", value: "code")
let clientId = URLQueryItem(name: "client_id", value: "") // TODO: Enter your OAuth client ID.
let redirectUri = URLQueryItem(name: "redirect_uri", value: "") // TODO: Enter the redirect URI of your OAuth app.
oauthUrlComp.queryItems = [responseType, clientId, redirectUri, codeChallenge, codeChallengeMethod]

let scheme = "" // TODO: Enter the custom scheme of the redirect URI of your OAuth app.

guard let oauthUrl = oauthUrlComp.url else { return }

let session = ASWebAuthenticationSession(url: oauthUrl, callbackURLScheme: scheme) { callbackUrl, error in
    self.handleAuthResult(callbackUrl: callbackUrl, error: error)
}

session.presentationContextProvider = self
session.start()

Since you need to provide an instance of ASWebAuthenticationPresentationContextProviding, you will also need to implement presentationAnchor in your ViewController.

extension ViewController : ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        return view.window!
    }
}
```

### Handle the response

After the OAuth flow redirects to your app via custom URL scheme, the completion handler you passed
into the `ASWebAuthenticationSession`'s constructor is invoked. From here, verify that authentication
was successful before requesting an access token.

```swift
private func handleAuthResult(callbackUrl: URL?, error: Error?) {
  guard let callbackUrl = callbackUrl else { return }
  if error == nil {
      guard let url = URLComponents(string: callbackUrl.absoluteString) else { return }
      if let errorParam = url.queryItems?.first(where: { $0.name == "error" })?.value {
          handleAuthError(errorParam)
          return
      }
      guard let code = url.queryItems?.first(where: { $0.name == "code" })?.value else { return }
      self.delegate.requestAccessToken(code: code, codeChallengeHelper: self.codeChallengeHelper)
  }
}
```

In your `AppDelegate`, handle the remainder of the OAuth flow starting with the `requestAccessToken`
method invoked in the previous code snippet. Include your client ID and redirect URI. As a reminder,
do not include hardcoded credentials in a production application.

```swift
private func buildAccessTokenBody(code: String, verifier: String?) -> Data? {
  var urlComp = URLComponents()
  urlComp.queryItems = [
      URLQueryItem(name: "grant_type", value: "authorization_code"),
      URLQueryItem(name: "code", value: code),
      URLQueryItem(name: "redirect_uri", value: ""), // TODO: Input your redirect URI
      URLQueryItem(name: "code_verifier", value: verifier)
  ]
  return urlComp.query?.data(using: .utf8)
}

func requestAccessToken(code: String, codeChallengeHelper: CodeChallengeHelper) {
    guard let url = URL(string: "https://zoom.us/oauth/token") else { return }
    let clientKey = "" // TODO: Enter the client ID from your Marketplace OAuth app.
    let clientSecret = "" // TODO: Enter the client secret from your Marketplace OAuth app. Not used with a public client ID.
    guard let encoded = "\(clientKey):\(clientSecret)".data(using: .utf8)?.base64EncodedString() else { return }

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.addValue("Basic \(encoded)", forHTTPHeaderField: "Authorization")
    request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
    request.httpBody = buildAccessTokenBody(code: code, verifier: codeChallengeHelper.verifier)

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard self.checkRequestResult(data: data, response: response, error: error) else { return }

        let response = try! JSONDecoder().decode(AccessTokenResponse.self, from: data!)
        self.handleAccessToken(response.access_token) // TODO: Store the token and use it on Zoom REST API calls as Authorization: Bearer <access_token>.
    }

    task.resume()
}

private struct AccessTokenResponse : Decodable {
    public let access_token: String
}
```

The returned `access_token` is a user-level OAuth 2.0 Bearer token. Send it on Zoom REST API requests
as `Authorization: Bearer <access_token>`. It will only be able to call endpoints covered by the
scopes granted to your Marketplace app.
