Rija Development: OAuth Authorization

Authorization Flow

When someone launches Rija for the first time, Rija must get a Jira access token that gives the app access to the person’s Jira account. This involves the following steps:

  1. Show a login view with a button to sign in to Jira.
  2. When the person clicks the button, a private browser opens.
  3. The person signs in to their Jira account.
  4. The person grants Rija permission to access their Jira account.
  5. Jira gives Rija an authorization code.
  6. Rija exchanges the authorization code for the access token.
  7. Rija saves the access token in the Keychain.

This post is going to focus on Steps 3–6.

Sign in to Jira with ASWebAuthenticationSession

The ASWebAuthenticationSession class simplifies logging into websites from Swift apps. To create a login session, create a ASWebAuthenticationSession object and supply three arguments:

  • An authorization URL
  • A callback URL
  • A callback closure

When the login session finishes successfully, the code in the callback closure runs.

Authorization URL

The authorization URL is the URL for the website where you’re logging in. Jira has the following URL for OAuth authorization:

https://auth.atlassian.com/authorize?
  audience=api.atlassian.com&
  client_id=YOUR_CLIENT_ID&
  scope=REQUESTED_SCOPE_ONE%20REQUESTED_SCOPE_TWO&
  redirect_uri=https://YOUR_APP_CALLBACK_URL&
  state=YOUR_USER_BOUND_VALUE&
  response_type=code&
  prompt=consent

I use the following code to set the URL to sign in to Jira:

var components = URLComponents()
components.scheme = "https"
components.host = "auth.atlassian.com"
components.path = "/authorize"
components.queryItems = [
  URLQueryItem(name: "audience", value: "api.atlassian.com"),
  URLQueryItem(name: "client_id", value: clientID),
  URLQueryItem(name: "scope", value: 
    "read:jira-user 
    read:jira-work 
    manage:jira-configuration 
    manage:jira-project 
    write:jira-work 
    offline_access"),
  URLQueryItem(name: "redirect_uri", value: callbackUrl + "://auth"),
  URLQueryItem(name: "state", value: UUID().uuidString),
  URLQueryItem(name: "response_type", value: "code"),
  URLQueryItem(name: "prompt", value: "consent")
]
            
guard let authURL = components.url else {
  return
}

Jira’s authorization URL is complex, as you can see by the amount of code needed to build the URL.

Callback URL

The callback URL is where you go when the login session finishes. For iOS and Mac apps the callback URL should take you back to the app.

Creating a callback URL requires you to add a custom URL type to your Info.plist file.

custom URL type

Enter the URL type in the URL Schemes text field. It should look like the following:

appname://

But when you supply the callback URL for the login session, you should omit the :// part of the URL. ASWebAuthenticationSession cannot handle colons in the callback URL.

let callbackUrl = "rija"

The ASWebAuthenticationSession Code

Using Apple’s ASWebAuthenticationSession class simplifies signing in to websites. The following code initializes and starts the login session:

var webAuthSession: ASWebAuthenticationSession?
    
webAuthSession = ASWebAuthenticationSession.init(url: authURL,  callbackURLScheme: callbackUrl, 
  completionHandler: { (callback:URL?, error:Error?) in
    
  guard error == nil, let successURL = callback else {
    return
  }
    
  // Get the authorization code.
  let query = URLComponents(string: (successURL.absoluteString))?
    .queryItems?.filter({$0.name == "code"}).first
                
  let authorizationCode = query?.value ?? ""
                
  // Exchange the authorization code for an access token and save it 
  // in the Keychain. I have to make the calls inside the 
  // callback to ensure there's a valid authorization 
  // code before doing anything else.
    
  // Have to wrap the code in a Task block because 
  // ASWebAuthenticationSession does not 
  // support Swift concurrency (async await).
  Task {
    if let token = await self.exchangeAuthorizationCodeFor
      AccessToken(code: authorizationCode) {
                        
      self.saveTokenInKeychain(token: token)
    }
  }
})
    
// Run the session
webAuthSession?.presentationContextProvider = self
webAuthSession?.prefersEphemeralWebBrowserSession = true
webAuthSession?.start()

The following article has more details on login sessions:

Log in to Websites with ASWebAuthenticationSession

Exchange the Authorization Code for the Access Token

After getting the authorization code from Jira, you have to exchange the code for an access token, which you use to make REST API calls in Jira. The exchange involves making a POST request to Jira, supplying the authorization code as part of the body. The following code demonstrates how to exchange the authorization code for an access token:

func exchangeForAccessToken(authorizationCode: String) async -> AuthToken? {
  var components = URLComponents()
  components.scheme = "https"
  components.host = "auth.atlassian.com"
  components.path = "/oauth/token"
            
  var request = URLRequest(url: components.url!)
  request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            
  let clientID = ProcessInfo.processInfo.environment["CLIENT_ID"]!
  let clientSecret = ProcessInfo.processInfo.environment["CLIENT_SECRET"]!
            
  request.httpMethod = "POST"
  let body = [
    "grant_type": "authorization_code",
    "client_id": clientID,
    "client_secret": clientSecret,
    "code": authorizationCode,
    "redirect_uri": "rija://auth"
  ]
  let bodyData = try? JSONSerialization.data(withJSONObject: body)
  request.httpBody = bodyData
            
  do {
    let (data, error) = try await URLSession.shared.data(
      for: request, delegate: nil)
    let decoder = JSONDecoder()
    return try decoder.decode(AuthToken.self, from: data)
  } catch {
    print(error)
  }
            
  return nil
}

About Rija

Rija is a Jira issue tracker under development for Mac (and possibly iOS). The following article provides more details on Rija:

Rija Development: Intro

Get the Swift Dev Journal Newsletter

Subscribe and get exclusive articles, a free guide on moving from tutorials to making your first app, notices of sales on books, and anything I decide to add in the future.

    We won't send you spam. Unsubscribe at any time.