DEVELOPERS BLOG

iOS and Android apps using AtHoc REST APIs

Before we begin today, look at my previous blog titled “New AtHoc v2 REST APIs” which explains how you can setup your AtHoc environment to use REST APIs in your application.

Once your environment is set up, you can start diving into the code. The first thing you’re going to need to do is authorize the user. This will allow you to use the ‘rest’ of the APIs to view devices, organizations, users, and of course, publish alerts.

There are three main types of authentication: password grant, authorization code grant, and implicit grant. This blog post will cover password grant and implicit grant. More information about the different types of authentication can be found in the “Authentication” section of the quick start guide located here. Once the user is authenticated and authorized, a bearer token is sent back which you pass into the headers of your future requests.

When should you use the different methods of authentication? For password grants, you need to program the client secret, the username, and password directly into the backend of the application. So, the question becomes, is the programmer trusted to keep that information safe? If so, then go right ahead. It also might be the only option in certain cases where there isn’t much of a front end. Realistically, this method should only be used if it is absolutely necessary

For implicit grant and code grant, the user is redirected to an authentication dialogue where they input their username and password. The user’s sensitive information is never actually dealt with in the code, so they don’t need to trust the programmer. This would be the preferred method in most cases.

Here is an example of how password grant authentication can be done in code.

Android:



/* For password grant authentication. The relevant info (client id, client secret, username, password, etc.) is sent in the parameters of the request. A bearer token is sent in the response. This can be used to to do other actions such as publishing a request */ public void passwordAuth(RequestQueue queue, String clientId, String clientSecret, String userId, String password) { String url = "https:///authservices/auth/connect/token"; // Set the parameters final Map params = new HashMap<>(); params.put("client_id", clientId); params.put("client_secret", clientSecret); params.put("grant_type", "password"); params.put("username", userId); params.put("password", password); params.put("acr_values", "tenant:"); params.put("scope", "openid profile athoc.iws.web.api"); // Make a post request StringRequest request = new StringRequest(Request.Method.POST, url, new Response.Listener() { @Override public void onResponse(String response) { try { // Yay! We did it! token = new JSONObject(response).getString("access_token"); Toast.makeText(getApplicationContext(), "Authorized", Toast.LENGTH_LONG).show(); } catch (JSONException e) { Log.e("AtHocTest", e.toString()); } } }, new Response.ErrorListener() { // Oops! Something happened! @Override public void onErrorResponse(VolleyError error) { Toast.makeText(getApplicationContext(), error.toString(), Toast.LENGTH_LONG).show(); } }) { // Add the parameters to the request @Override protected Map getParams() { return params; } }; queue.add(request); }
iOS:


func passwordGrant(clientId: String, clientSecret: String, userId: String, password: String){ // Configure the POST request to send the credentials and get the token let url = URL(string: "https:///authservices/auth/connect/token")! var request = URLRequest(url: url) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" let parameters: [String: Any] = [ "client_id": clientId, "client_secret": clientSecret, "grant_type": "password", "username": userId, "password": password, "acr_values": "tenant:", "scope": "openid profile athoc.iws.web.api" ] // Properly format the parameters request.httpBody = parameters.percentEscaped().data(using: .utf8) // Make the request let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data, error == nil else { // check for fundamental networking error self.showMessage(message: error?.localizedDescription ?? "Unknown error") return } do { // Get the response and parse out the token if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { if let temp = json["access_token"] as? String { self.showMessage(message: "Authorized!") self.token = temp } } } catch { self.showMessage(message: "Something went wrong!") } } task.resume() }
The following code will also need to be added to properly format the URL:


// Extra utilities to format the request parameters properly extension Dictionary { func percentEscaped() -> String { return map { (key, value) in let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" return escapedKey + "=" + escapedValue } .joined(separator: "&") } } extension CharacterSet { static let urlQueryValueAllowed: CharacterSet = { // does not include "?" or "/" due to RFC 3986 - Section 3.4 let generalDelimitersToEncode = ":#[]@" let subDelimitersToEncode = "!$&'()*+,;=" var allowed = CharacterSet.urlQueryAllowed allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") return allowed }() }

As you can see, a simple POST request is made, sending all the relevant info. The client ID and client secret are from the API application you created on the AtHoc management system. The username and password are your username and password for the AtHoc management system. For the acr_values, you will put in tenant<your_organization_name>.

Another way to get authenticated is through an Implicit Grant. This method uses OAuth 2.0 and will redirect to an authentication dialogue in a browser and ask the user to sign in. Once the user has signed in, the token is sent back as a parameter in the redirect URI which links back to the app.

The first step to making an implicit grant is directing the user to the authentication dialogue in the browser.

Android:



/* Authenticate a user using implicit authentication. The user will be taken to the AtHoc management authentication dialogue to sign in. Once authenticated, they will be taken back to the application */ public void authImplicit(String clientId) { Uri.Builder builder = new Uri.Builder(); builder.scheme("https") .authority("") .appendPath("AuthServices") .appendPath("Auth") .appendPath("connect") .appendPath("authorize") .appendQueryParameter("client_id", clientId) .appendQueryParameter("redirect_uri", "com.example.athoctest://") .appendQueryParameter("scope", "athoc.iws.web.api") .appendQueryParameter("acr_values", "tenant:") .appendQueryParameter("response_type", "token"); // Make an intent to the URL. This will actually take you to the desired webpage Intent intent = new Intent(Intent.ACTION_VIEW, builder.build()); if(intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } }
iOS:


@IBAction func authorizeButtonTapped(_ sender: UIButton) { let redirectUri = "com.example.athoctest://" let clientId = "client_ID" // Go to login dialogue if let url = URL(string: "https:///AuthServices/Auth/connect/authorize?client_id=" + clientId + "&redirect_uri=" + redirectUri + "&scope=athoc.iws.web.api&acr_values=tenant:&response_type=token") { UIApplication.shared.open(url) } }

Simply build the URL with the correct parameters (client id, scope, acr values, response type, and redirect URI). Add this to the intent filter and start the intent and that’s it! But the question is, how do you properly configure your redirect URI so that once you log in, you’re taken back to your app?

In your AndroidManifest.xml, add the following under the activity tag:

 

<intent-filter>  

<action android:name="android.intent.action.VIEW"/>  

<category android:name="android.intent.category.DEFAULT"/>  

<category android:name="android.intent.category.BROWSABLE"/>  

<data  android:scheme="com.example.athoctest"/>  

</intent-filter>

For iOS, you can follow these steps to configure the redirect URI:

1.     Click on the project file to the left

2.     Click on the "Info" tab.

3.     Go all the way down and expand the "URL Types"

4.     Add your URL scheme

Note: The scheme can really be whatever you want. You can also add a host if you want, but it is not necessary.

Now, your redirect URI is simply <scheme>://<host>. If you did not specify a host, leave it blank. It must match what was put in the API application when you setup your environment.

Now, for the app to be able to handle the redirect and extract the token from the URI, you can do the following:

Android:



/* We are taken here when the app starts/restarts */ @Override protected void onResume() { super.onResume(); Uri uri = getIntent().getData(); // Once we come back to the app from the browser after having logged in, we can check that we get // what we're expecting. Then grab the token if(uri != null && uri.toString().startsWith("com.example.athoctest")) { token = getToken(uri.toString()); } } /* Simple method to parse the URI */ private String getToken(String url) { int equals = url.indexOf('='); int amp = url.indexOf('&'); return url.substring(equals + 1, amp); }
iOS:


// This function is executed when we return to the app from the login dialogue. It is implemented in the AppDelegate file func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool{ token = getToken(url: url.absoluteString) return true } // Simple function to parse the returned URI func getToken(url: String) -> String { let equals = url.firstIndex(of: "=") let amp = url.firstIndex(of: "&") return String(url[url.index(equals!, offsetBy: 1) ..< amp!]) }

I wrote a quick function to parse the URI but it only works in this specific case. You can parse the URI as needed to extract the access token.

With both the password grant and the implicit grant, the token is sent in a JSON blurb which is then parsed and stored in a global variable so it can be used in future requests. Now we can use this to send an alert!

Android:



/* Publishes an AtHoc alert. */ public void publish(RequestQueue queue, String commonName) { String url = "https:///api/v2/orgs//alerts"; // Add the template common name to the parameters // The name can be found in the alert template created in the AtHoc management system Map params = new HashMap<>(); params.put("TemplateCommonName", commonName); // Make a post request JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, new JSONObject(params), new Response.Listener() { @Override public void onResponse(JSONObject response) { // We did it! Toast.makeText(getApplicationContext(), "success", Toast.LENGTH_LONG).show(); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // Something went wrong :( Toast.makeText(getApplicationContext(), error.toString(), Toast.LENGTH_LONG).show(); } }) { @Override public Map getHeaders(){ // Add the bearer token to the headers Map headers = new HashMap<>(); headers.put("Content-Type","application/json; charset=utf-8"); headers.put("Authorization", "bearer " + token); return headers; } }; queue.add(request); }
iOS:


@IBAction func publishButtonTapped(_ sender: UIButton) { if appDelegate.token == ""{ if token == nil { showMessage (message: "Unauthorized") return } } else { // grab the token from the app delegate for implicit grant token = appDelegate.token appDelegate.token = "" } // Configure the POST request to publish an alert let url = URL(string: "https:///api/v2/orgs//alerts")! var request = URLRequest(url: url) request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.setValue("bearer " + token!, forHTTPHeaderField: "Authorization") request.httpMethod = "POST" let json: [String: Any] = [ "templateCommonName": "common_name" ] let jsonData = try? JSONSerialization.data(withJSONObject: json) request.httpBody = jsonData // Make the request, check for errors let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { self.showMessage(message: error?.localizedDescription ?? "Unknown error") return } if (response.statusCode == 200) { self.showMessage(message: "Success") } else if (response.statusCode == 401) { self.showMessage(message: "Unauthorized") } } task.resume() }

Again, we make a simple POST request. The commonName parameter is the common name of the alert template that you’re using. This can be found on the AtHoc management system. This time, we’re sending in JSON in the request body, so we’ll use a JsonObjectRequest. We also need to send in the bearer token we retrieved earlier in order to publish the request.

Note: You only need the TemplateCommonName parameter to publish an alert, but you can add/modify the alert title, alert body, severity, and a lot more by adding some more information in the JSON request body. The details of what you can send and how to send it, along with all the different API calls you can make with AtHoc v2, can be found at https://<server>/api/v2/docs

And that’s it! Once you’ve done this, you should receive a text on your phone, or an email, or however you set up the alert template.

Nathaniel Johnston

About Nathaniel Johnston

Enterprise Solutions Development Student - IoT