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:
- Show a login view with a button to sign in to Jira.
- When the person clicks the button, a private browser opens.
- The person signs in to their Jira account.
- The person grants Rija permission to access their Jira account.
- Jira gives Rija an authorization code.
- Rija exchanges the authorization code for the access token.
- 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.
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: