SSP OAuth2 API Authentication

Overview

Beginning with SSP 2.0.0, third party clients can authenticate with its RESTful API using OAuth2 Client Credentials Grants as defined by draft 31 of that specification.

Client Credentials Grants are primarily useful for server-to-server integrations where a third-party application is given access to SSP data and operations without requiring explicit, per end-user authorizations. For example, it would allow server-side portlet code to query SSP for any user's current task list or MAP courses without prompting the end user to either log in to SSP or authorize these specific operations. This results in client applications having rather broad access to SSP data, but is consistent with the current design of SSP's APIs, which are typically protected by per-operation permissions rather than per-user permissions (e.g. "read any MAP", not "read only my MAPs").

Client Credentials Flow

The following diagram illustrates the high-level flow of SSP API authentication via OAuth2 Client Credentials Grants. For a third party application, the most relevant components are:

  1. Client - The third party application itself,
  2. Access Token API - A SSP-hosted component represented by the /api/1/oauth2/token URI. The Client sends POST requests to that resource to obtain Tokens which authorize subsequent requests to the rest of the API. These POST requests contain the application's credentials encoded via HTTP Basic Auth. The response is a JSON payload containing the Token itself and a minimal amount of metadata.
  3. Other APIs - The standard SSP APIs. Clients in possession of a Token can include that value as a request header and will be granted permissions associated with that Token. SSP administrators can configure Token-permission relationships and revoke tokens via the SSP user interface.

Client Configuration

Client Configuration Step 0 - Make Sure oauth2_client_password_encoding_secret Is Set

OAuth2 Client secrets are hashed using the value of oauth2_client_password_encoding_secret as set in ssp-config.properties. Make sure that confg is set to something you plan to use for the long term as changes to it will make existing secrets unusable. Changes to ssp-config.properties require an application restart.

Client Configuration Step 1 - Navigate to "OAuth2 Clients" Admin Tool

To see data in this tool, the current user must have the API_KEY_READ permission. To change/add data, they need API_KEY_WRITE. Be conservative in granting these permissions, especially the latter, because it allows users to create "back doors" to the application. In particular, users can grant any permission to any OAuth2 Client, not just permissions they current hold.

Click the "Add" button to create a new Client:

Client Configuration Step 2 - Complete the OAuth2 Client Creation Form

The "Client ID" field is actually stored as a SSP username, so be sure to pick a value that is unlikely to conflict with a "real" user's username.

Be sure to take a note of the "Secret" value before saving. The plaintext secret is not needed (e.g. for validating signatures, as with a OAuth1 Consumer Secret), so it is treated the same way you'd expect for an end-user's password. Specifically, a one-way hash is applied to the secret before it is written to the database, which means it is impossible for the application to remind you of the plaintext value.

First Name and Last Name are required for OAuth2 Clients because they act just like "real" users when interacting with SSP data and the SSP APIs always include the first and last names of the creating and most-recently-modifying users for any given record.

Click "Save" to create your new Client:

Client Configuration Step 3 - Browse the Updated Client List

If the save was successful, the new Client list should resemble the following:

Client Configuration Step 4 - Edit the Client

If you decide you need to make changes to the Client, double click the corresponding row in the list view.

Note that changes to any of the following fields will immediately invalidate existing tokens for the editied client, forcing clients to re-authenticate:

  1. Client ID
  2. Secret
  3. Permissions
  4. Active

Use the Active checkbox to disable a Client without losing any of its configuration.

Sample Client

The following example demonstrates usage of SSP's Client Credentials Grant flow using the Groovy HTTPBuilder library with corresponding curl commands to accomplish the same thing albeit without response parsing.

The complete file is attached.

Sample Client Step 1 - Groovy Setup

First install a recent version of Groovy and make sure you can invoke it from the command line.

Create a file called ssp_oauth2_client.groovy and add the following at the top:

@GrabResolver( name='codehaus.snapshot', root='http://snapshots.repository.codehaus.org', m2compatible='true' )
@Grapes([
@Grab(group='org.codehaus.groovy.modules.http-builder', module='http-builder', version='0.7.1' )
])
import groovyx.net.http.HTTPBuilder
import static groovyx.net.http.ContentType.URLENC
import static groovyx.net.http.ContentType.JSON
import static groovyx.net.http.Method.POST
import static groovyx.net.http.Method.GET
 
println "Hello World" 

Then run the file to make sure dependencies can be properly downloaded. This may take several seconds as the downloads proceed, during which time you will see no output.


Sample Client Step 2 - Create the Client and Request an Access Token

Remove the println "Hello World" and add this in its place, substituting values appropriate for your installation into the variables defined at the top:

def baseUrl = 'http://localhost:8080'
def clientId = 'my-client'
def clientSecret = 'my-client-secret'

def site = new HTTPBuilder(baseUrl)
site.auth.basic(clientId, clientSecret)
def accessToken
site.request ( POST ) {
    uri.path = '/ssp/api/1/oauth2/token'
    send(URLENC, [grant_type:'client_credentials'])
    response.failure = { resp, json ->
        println("Token error response (${resp.statusLine.statusCode}): ${json}")
        System.exit(1)
    }
    response.success = { resp, json ->
        accessToken = json['access_token']
        println("Token response: ${json}")
    }
}

The command and its output should look like this:

$> groovy ssp_oauth2_client.groovy 
Token response: [expires_in:43199, token_type:bearer, access_token:9dc24b1e-9dd0-4af9-971d-e1255ded3a16]

The site.request() invocation resulted in a POST request similar to the following:

POST /ssp/api/1/oauth2/token HTTP/1.1
Host: localhost
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials

Groovy parsed the original response JSON into a Groovy Map. The actual response resembled:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"9dc24b1e-9dd0-4af9-971d-e1255ded3a16",
  "token_type":"bearer",
  "expires_in":43199,
}

That access_token value, which we've stored in the accessToken Groovy variable can then be used to invoke any other API for which this client has permissions.

Requesting an Access Token with curl

The same thing can be accomplished via curl:

$> curl "http://my-client:my-client-secret@localhost:8080/ssp/api/1/oauth2/token" -d grant_type=client_credentials -w "\n"
{"access_token":"9dc24b1e-9dd0-4af9-971d-e1255ded3a16","token_type":"bearer","expires_in":39956}

Run with the '--trace-ascii -' option to see exactly what's going on.

Sample Client Step 3 - Invoke a SSP API

Set the value of accessToken into the Authorization header of subsequent API requests and those requests will be processed using the permission set associated with that Token. For simplicity, here we just request the "top 1" result of the Campus API:

site.auth.basic("","") // clear out basic auth for good measure
site.request( GET ) {
    uri.with {
        path = '/ssp/api/1/reference/campus'
        query = ["limit": 1]
    }
    headers.'Authorization' = "Bearer ${accessToken}"
    response.failure = { resp, json ->
        println("API error response (${resp.statusLine.statusCode}): ${json}")
        System.exit(1)
    }
    response.success = { resp, json ->
        println("API response: ${json}")
    }
}

The output should resemble the following (note the Token reuse, even when running the entire script again). Line breaks added for readability:

$> groovy ssp_oauth2_client.groovy 
Token response: [expires_in:42579, token_type:bearer, access_token:9dc24b1e-9dd0-4af9-971d-e1255ded3a16]
API response: [message:, results:4, success:true, rows:[[id:0a000085-401c-11ff-8140-1d0826cf000e, earlyAlertCoordinatorId:9000dfd8-d78a-4d22-bec4-e2c1be85aae8,
createdBy:[id:0a0f106a-3f7d-1448-813f-7d3e7a130001, lastName:Administrator, firstName:Amy], description:, modifiedBy:[id:0a0f106a-3f7d-1448-813f-7d3e7a130001,
lastName:Administrator, firstName:Amy], name:Campus Foo, objectStatus:ACTIVE, createdDate:1374876608207, modifiedDate:1374876608207]]]

The actual request resembled this:

GET /ssp/api/1/reference/campus HTTP/1.1
Host: localhost
Authorization: Bearer 9dc24b1e-9dd0-4af9-971d-e1255ded3a16

The response, which was parsed from JSON to a Groovy Map in the output above is no different than a standard SSP API response 

Invoking an Access Token-authorized API with curl

Here is how the same invocation could be performed with curl:

$> curl --header "Authorization: Bearer 9dc24b1e-9dd0-4af9-971d-e1255ded3a16" "http://localhost:8080/ssp/api/1/reference/campus?limit=1" -w "\n"
{"success":"true","message":"","results":4,"rows":[{"id":"0a000085-401c-11ff-8140-1d0826cf000e","createdDate":1374876608207,
"createdBy":{"id":"0a0f106a-3f7d-1448-813f-7d3e7a130001","firstName":"Amy","lastName":"Administrator"},"modifiedDate":1374876608207,
"modifiedBy":{"id":"0a0f106a-3f7d-1448-813f-7d3e7a130001","firstName":"Amy","lastName":"Administrator"},"objectStatus":"ACTIVE",
"name":"Campus Foo","description":"","earlyAlertCoordinatorId":"9000dfd8-d78a-4d22-bec4-e2c1be85aae8"}]}

Errors

If the client credentials are incorrect, the output from the Groovy script above will resemble the following:

$> groovy ssp_oauth2_client.groovy 
Token error response (401): [error:unauthorized, error_description:Bad credentials]

Or from curl:

$> curl "http://my-client:my-client-secret2@localhost:8080/ssp/api/1/oauth2/token" -d grant_type=client_credentials -w "\n"
{"error":"unauthorized","error_description":"Bad credentials"}

If the credentials were correct, but the client is disabled:

$> groovy ssp_oauth2_client.groovy 
Token error response (401): [error:unauthorized, error_description:User is disabled]

If the credentials were correct, the client is enabled, but lacks the necessary permissions for the API call:

$> groovy ssp_oauth2_client.groovy 
Token response: [expires_in:43199, token_type:bearer, access_token:25a8ba01-f32a-4ce7-9ca9-3bedf2b2dc08]
API error response (403): [message:Access is denied, success:false]

If the Token has expired (in which case the caller should just request a new Token from /api/1/oauth2/token):

$> groovy ssp_oauth2_client.groovy 
API error response (401): [error:invalid_token, error_description:Invalid access token: 25a8ba01-f32a-4ce7-9ca9-3bedf2b2dc08]

Same thing with curl:

$> curl --header "Authorization: Bearer 25a8ba01-f32a-4ce7-9ca9-3bedf2b2dc08" "http://localhost:8080/ssp/api/1/reference/campus?limit=1" -w "\n"
{"error":"invalid_token","error_description":"Invalid access token: 25a8ba01-f32a-4ce7-9ca9-3bedf2b2dc08"}