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:
- Client - The third party application itself,
- Access Token API - A SSP-hosted component represented by the
/api/1/oauth2/token
URI. The Client sends POST requests to that resource to obtainTokens
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 theToken
itself and a minimal amount of metadata. - 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 thatToken
. SSP administrators can configureToken
-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:
- Client ID
- Secret
- Permissions
- 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"}