HTTP or HTTPS
By default,the OAuth2 protocol requires all calls to be performed using HTTPS. You can read how to configure Apache Tomcat to enable HTTPShere.
For test purposes,you may want to configure ORDS to allow OAuth over HTTP. Edit the "/u01/ords/conf/ords/defaults.xml",adding the following entry.
<entry key="security.verifySSL">false</entry>
Restart Tomcat for the change to take effect.
$CATALINA_HOME/bin/shutdown.sh $CATALINA_HOME/bin/startup.sh
Create a Test Database User
We need a new database user for our testing.
CONN / AS SYSDBA ALTER SESSION SET CONTAINER=pdb1; DROP USER testuser1 CASCADE; CREATE USER testuser1 IDENTIFIED BY testuser1 DEFAULT TABLESPACE users QUOTA UNLIMITED ON users; GRANT CREATE SESSION,CREATE TABLE TO testuser1;
Create and populate a copy of the EMP table.
CONN testuser1/testuser1@pdb1 CREATE TABLE EMP ( EMPNO NUMBER(4,0),ENAME VARCHAR2(10 BYTE),JOB VARCHAR2(9 BYTE),MGR NUMBER(4,HIREDATE DATE,SAL NUMBER(7,2),COMM NUMBER(7,DEPTNO NUMBER(2,CONSTRAINT PK_EMP PRIMARY KEY (EMPNO) ); insert into EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO) values (7369,'SMITH','CLERK',7902,to_date('17-DEC-80','DD-MON-RR'),800,null,20); insert into EMP (EMPNO,DEPTNO) values (7499,'ALLEN','SALESMAN',7698,to_date('20-FEB-81',1600,300,30); insert into EMP (EMPNO,DEPTNO) values (7521,'WARD',to_date('22-FEB-81',1250,500,DEPTNO) values (7566,'JONES','MANAGER',7839,to_date('02-APR-81',2975,DEPTNO) values (7654,'MARTIN',to_date('28-SEP-81',1400,DEPTNO) values (7698,'BLAKE',to_date('01-MAY-81',2850,DEPTNO) values (7782,'CLARK',to_date('09-JUN-81',2450,10); insert into EMP (EMPNO,DEPTNO) values (7788,'SCOTT','ANALYST',7566,to_date('19-APR-87',3000,DEPTNO) values (7839,'KING','PRESIDENT',to_date('17-NOV-81',5000,DEPTNO) values (7844,'TURNER',to_date('08-SEP-81',1500,DEPTNO) values (7876,'ADAMS',7788,to_date('23-MAY-87',1100,DEPTNO) values (7900,'JAMES',to_date('03-DEC-81',950,DEPTNO) values (7902,'FORD',DEPTNO) values (7934,'MILLER',7782,to_date('23-JAN-82',1300,10); commit;Enable ORDS and Create a Web Service
Enable REST web services for the test schema. We could use any unique and legal URL mapping pattern for the schema,so it is not necessary to expose the schema name as we have done here.
CONN testuser1/testuser1@pdb1 BEGIN ORDS.enable_schema( p_enabled => TRUE,p_schema => 'TESTUSER1',p_url_mapping_type => 'BASE_PATH',p_url_mapping_pattern => 'testuser1',p_auto_rest_auth => FALSE ); COMMIT; END; /
Define a basic web service we can use for the tests.
CONN testuser1/testuser1@pdb1 BEGIN ORDS.define_service( p_module_name => 'testmodule1',p_base_path => 'testmodule1/',p_pattern => 'emp/:empno',p_method => 'GET',p_source_type => ORDS.source_type_query,p_source => 'SELECT * FROM emp WHERE empno = :empno',p_items_per_page => 0); COMMIT; END; /
The web service will now be available from the following URLs without authentication.
http://ol7-121.localdomain:8080/ords/pdb1/testuser1/testmodule1/emp/7788 https://ol7-121.localdomain:8443/ords/pdb1/testuser1/testmodule1/emp/7788
For the rest of the article I will assume HTTPS has been enabled. If you've enabled HTTP access to OAUTH,then simply change the test URLs accordingly for the tests. The "curl" command below uses the "-k" option to prevent the call from verifying the self-signed certificate used on this server. We can see the expected JSON data is returned.
$ curl -i -k https://ol7-121.localdomain:8443/ords/pdb1/testuser1/testmodule1/emp/7788
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: "jtC17IXyetESUjSkxB2ani/a1TnFh28yfor+fLmxxUzGr6G9IFxQ77+/Gd71W4Qzz0rSxf90Qqbl+ICwezTayQ=="
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed,29 Jun 2016 08:35:50 GMT
{"items":[{"empno":7788,"ename":"SCOTT","job":"ANALYST","mgr":7566,"hiredate":"1987-04-18T23:00:00Z","sal":3000,"comm":null,"deptno":20}]}
$
Now let's start setting up authentication.
ORDS Roles and PrivilegesTo protect the web service,we need to create a role with an associated privilege,then map the privilege to the web service. Normally,we would expect a role to be a collection of privileges,and of course a single privilege can be part of multiple roles,but in this case we will keep it simple. The following code creates a new role called "emp_role".
CONN testuser1/testuser1@pdb1 BEGIN ORDS.create_role( p_role_name => 'emp_role' ); COMMIT; END; / -- Display the role. COLUMN name FORMAT A20 SELECT id,name FROM user_ords_roles WHERE name = 'emp_role'; ID NAME ---------- -------------------- 10312 emp_role SQL>
We create a new privilege called "emp_priv",which is associated with the role.
CONN testuser1/testuser1@pdb1 DECLARE l_arr OWA.vc_arr; BEGIN l_arr(1) := 'emp_role'; ORDS.define_privilege ( p_privilege_name => 'emp_priv',p_roles => l_arr,p_label => 'EMP Data',p_description => 'Allow access to the EMP data.' ); COMMIT; END; / -- Display the privilege. COLUMN name FORMAT A20 SELECT id,name FROM user_ords_privileges WHERE name = 'emp_priv'; ID NAME ---------- -------------------- 10313 emp_priv SQL> -- Display the privilege-role relationship. COLUMN privilege_name FORMAT A20 COLUMN role_name FORMAT A20 SELECT privilege_id,privilege_name,role_id,role_name FROM user_ords_privilege_roles WHERE role_name = 'emp_role'; PRIVILEGE_ID PRIVILEGE_NAME ROLE_ID ROLE_NAME ------------ -------------------- ---------- -------------------- 10313 emp_priv 10312 emp_role SQL>
BEGIN ORDS.create_privilege_mapping( p_privilege_name => 'emp_priv',p_pattern => '/testmodule1/emp/*' ); COMMIT; END; / -- Display mapping. COLUMN name FORMAT A20 COLUMN pattern FORMAT A20 SELECT privilege_id,name,pattern FROM user_ords_privilege_mappings WHERE name = 'emp_priv'; PRIVILEGE_ID NAME PATTERN ------------ -------------------- -------------------- 10246 emp_priv /testmodule1/emp/* SQL>
Once this mapping is in place,we can no longer access the web service without authentication. At this point we've not defined how we should authenticate,only that authentication is needed to access this resource.
$ curl -i -k https://ol7-121.localdomain:8443/ords/pdb1/testuser1/testmodule1/emp/7788
HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
Content-Type: text/html
Content-Length: 11577
Date: Wed,29 Jun 2016 08:45:32 GMT
.
. Edited out for brevity.
.
$
First-Party Authentication (Basic Authentication)
If you have to support basic authentication,it is possible by creating users on the ORDS server.
Create a new ORDS user called "emp_user" with access to the "emp_role" role.
$ cd /u01/ords $ $JAVA_HOME/bin/java -jar ords.war user emp_user emp_role Enter a password for user emp_user: Confirm password for user emp_user: Jun 29,2016 11:52:42 AM oracle.dbtools.standalone.ModifyUser execute INFO: Created user: emp_user in file: /u01/ords/conf/ords/credentials $
Access the web service from a browser using the following URL.
https://ol7-121.localdomain:8443/ords/pdb1/testuser1/testmodule1/emp/7788
You are presented with a 401 message,which includes a "sign in" link. Click the link,sign in with the ORDS credentials you just created and you will be directed to web service output.
Alternatively,specify the credentials in a "curl" command.
$ curl -i -k --user emp_user:Password1 https://ol7-121.localdomain:8443/ords/pdb1/testuser1/testmodule1/emp/7788
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: "jtC17IXyetESUjSkxB2ani/a1TnFh28yfor+fLmxxUzGr6G9IFxQ77+/Gd71W4Qzz0rSxf90Qqbl+ICwezTayQ=="
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat,02 Jul 2016 06:19:47 GMT
{"items":[{"empno":7788,"deptno":20}]}
$
OAuth 2
There are several methods of authenticating to the web service using OAuth 2. Before starting each example,make sure you clean up the OAUTH metadata,as described in theDeleting OAUTH Metadatasection. You do not need to delete the web service itself or the ORDS role,privilege and URL mapping. Each authentication example assumes these are already in place.
OAuth 2 revolves around registering clients,which represent a person or an application wanting to access the resource,then associating those clients to roles. Once the client is authenticated,it has access to the protected resources associated with the roles.
Many of the parameters in calls to theOAUTH
package are optional,but cause problems down the line if they are omitted. TheOAUTH
package calls in the following examples are the simplest I could make them without causing failures. When calling theCREATE_CLIENT
procedure,theP_PRIVILEGE_NAMES
parameter is mandatory,but it will accept dummy text if you don't want to associate a privilege directly to the client.
OAuth 2 : Client Credentials
The client credentials flow is a two-legged process that seems the most natural to me as I mostly deal with server-server communication,which should have no human interaction. For this flow we use the client credentials to return an access token,which is used to authenticate calls to protected resources. The example steps through the individual calls,but in reality it would be automated by the application.
Remember to clean up the OAUTH Metadata,183); text-decoration-line: none;" rel="nofollow">Deleting OAUTH Metadatasection.
Create a client with the grant type of "client_credentials".
CONN testuser1/testuser1@pdb1 BEGIN OAUTH.create_client( p_name => 'Emp Client',p_grant_type => 'client_credentials',p_owner => 'My Company Limited',p_description => 'A client for Emp management',p_support_email => 'tim@example.com',p_privilege_names => 'emp_priv' ); COMMIT; END; / -- Display client details. COLUMN name FORMAT A20 SELECT id,client_id,client_secret FROM user_ords_clients; ID NAME CLIENT_ID CLIENT_SECRET ---------- -------------------- -------------------------------- -------------------------------- 10316 Emp Client 3NvJRo_a0UwGKx7Q-kivtA.. F5WVwyrWxXj3ykmhSONldQ.. sql> -- Display client-privilege relationship. SELECT name,client_name FROM user_ords_client_privileges; NAME CLIENT_NAME -------------------- ------------------------------ emp_priv Emp Client sql>
Associate the client with the role that holds the correct privileges for the resources it needs to access.
BEGIN OAUTH.grant_client_role( p_client_name => 'Emp Client',p_role_name => 'emp_role' ); COMMIT; END; / -- Display client-role relationship. COLUMN client_name FORMAT A30 COLUMN role_name FORMAT A20 SELECT client_name,role_name FROM user_ords_client_roles; CLIENT_NAME ROLE_NAME ------------------------------ -------------------- Emp Client emp_role sql>
In order to access the web service,we must first retrieve an access token using theCLIENT_ID
andCLIENT_SECRET
we queried from theUSER_ORDS_CLIENTS
view.
CLIENT_ID : 3NvJRo_a0UwGKx7Q-kivtA.. CLIENT_SECRET : F5WVwyrWxXj3ykmhSONldQ.. OAUTH URL : https://ol7-121.localdomain:8443/ords/pdb1/testuser1/oauth/token
The example below retrieves the access token. Notice the user format of "CLIENT_ID:CLIENT_SECRET". It is easy to miss the ":" when you look at this for the first time.
$ curl -i -k --user 3NvJRo_a0UwGKx7Q-kivtA..:F5WVwyrWxXj3ykmhSONldQ.. --data "grant_type=client_credentials" https://ol7-121.localdomain:8443/ords/pdb1/testuser1/oauth/token HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Frame-Options: SAMEORIGIN Content-Type: application/json Transfer-Encoding: chunked Date: Wed,29 Jun 2016 12:07:02 GMT {"access_token":"-zYl-sFyB2iLicAHw2TsRA..","token_type":"bearer","expires_in":3600} $
We can now use the access token to call our web service. Notice the "Authorization: Bearer {access-token}" entry in the header of the call.
$ curl -i -k -H"Authorization: Bearer -zYl-sFyB2iLicAHw2TsRA.." https://ol7-121.localdomain:8443/ords/pdb1/testuser1/testmodule1/emp/7788 HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ETag: "jtC17IXyetESUjSkxB2ani/a1TnFh28yfor+fLmxxUzGr6G9IFxQ77+/Gd71W4Qzz0rSxf90Qqbl+ICwezTayQ==" Content-Type: application/json Transfer-Encoding: chunked Date: Wed,29 Jun 2016 12:07:31 GMT {"items":[{"empno":7788,sans-serif; font-size: 16px;">We successfully accessed the protected web service. OAuth 2 : Authorization CodeThe authorization code flow is a three-egged process. The user accesses a URL in a browser,which prompts for credentials. Once authenticated,the browser is redirected to a specified page with an authhorization code as one of the parameters in the URL. That authorization code is used in a call to generate an access token,which is used to authenticate calls to protected resources. With the exception of the user confirmation,all the other steps in the flow should be handled by the application. All the steps will be presented separately in the example that follows.
Create a client using the grant type of "authorization_code". The redirect and support URLs are not real,but we will be able to follow the example through anyway.
Box-sizing: border-Box; overflow: auto; font-family: Menlo,p_grant_type => 'authorization_code',p_redirect_uri => 'https://ol7-121.localdomain:8443/ords/pdb1/testuser1/redirect',p_support_uri => 'https://ol7-121.localdomain:8443/ords/pdb1/testuser1/support',p_privilege_names => 'emp_priv' ); COMMIT; END; / -- Display client details. COLUMN name FORMAT A20 SELECT id,client_secret FROM user_ords_clients; ID NAME CLIENT_ID CLIENT_SECRET ---------- -------------------- -------------------------------- -------------------------------- 10333 Emp Client gxqNSyxPbLUJhSj1yBe8qA.. E-_mKJBlOTfTdHc_zISniA.. sql>
We then attempt to request an authorization code. Notice we are using theCLIENT_ID
from theUSER_ORDS_CLIENTS
view along with a unique string that will represent the state.
CLIENT_ID : gxqNSyxPbLUJhSj1yBe8qA.. State : 3668D7A713E93372E0406A38A8C02171 URL : https://ol7-121.localdomain:8443/ords/pdb1/testuser1/oauth/auth?response_type=code&client_id={client_id}&state={state}
Access the following URL from a browser.
https://ol7-121.localdomain:8443/ords/pdb1/testuser1/oauth/auth?response_type=code&client_id=gxqNSyxPbLUJhSj1yBe8qA..&state=3668D7A713E93372E0406A38A8C02171
Box-sizing: border-Box; margin-top: 0px; margin-bottom: 11px; font-family: "Helvetica Neue",sign in with the ORDS credentials you created ealier (emp_user) and you will be directed to an approval page. Click the "Approve" button,which will take you to the redirect page you specified for the client.
The redirect page we specified for the client doesn't really exist,but we can get the authorization code and state from the URL.
https://ol7-121.localdomain:8443/ords/pdb1/testuser1/redirect?code=FF-APuIMukuBlrver1XU2A..&state=3668D7A713E93372E0406A38A8C02171
The application should check the state string matches the one used in the initial call. We use the authorization code to retrieve the access token.
CLIENT_ID : gxqNSyxPbLUJhSj1yBe8qA.. CLIENT_SECRET : E-_mKJBlOTfTdHc_zISniA.. User : CLIENT_ID:CLIENT_SECRET Data : grant_type=authorization_code&code={authorization-code} URL : https://ol7-121.localdomain:8443/ords/pdb1/testuser1/oauth/token
The following call retrieves the access token.
$ curl -i -k --user gxqNSyxPbLUJhSj1yBe8qA..:E-_mKJBlOTfTdHc_zISniA.. --data "grant_type=authorization_code&code=FF-APuIMukuBlrver1XU2A.." https://ol7-121.localdomain:8443/ords/pdb1/testuser1/oauth/token HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Frame-Options: SAMEORIGIN Content-Type: application/json Transfer-Encoding: chunked Date: Wed,29 Jun 2016 12:38:52 GMT {"access_token":"cOYb2hFK_SyxOh8o9n6R7A..","expires_in":3600,"refresh_token":"RC33rvSwAfhguraOWlvgfA.."} $
We can now access the protected resource using the access token.
$ curl -i -k -H"Authorization: Bearer cOYb2hFK_SyxOh8o9n6R7A.." https://ol7-121.localdomain:8443/ords/pdb1/testuser1/testmodule1/emp/7788
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: "jtC17IXyetESUjSkxB2ani/a1TnFh28yfor+fLmxxUzGr6G9IFxQ77+/Gd71W4Qzz0rSxf90Qqbl+ICwezTayQ=="
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed,29 Jun 2016 12:40:34 GMT
{"items":[{"empno":7788,"deptno":20}]}
$
OAuth 2 : Implicit
The implicit flow is a two-legged process that requires user interaction. The user accesses a URL in a browser,the browser is redirected to a specified page with an access token as one of the parameters in the URL. That access token is used to authenticate calls to protected resources. The example steps through the individual calls,but in reality everything but the user interaction would be automated by the application.
Create a client using the grant type of "implicit". The redirect and support URLs are not real,p_grant_type => 'implicit',client_secret FROM user_ords_clients; ID NAME CLIENT_ID CLIENT_SECRET ---------- -------------------- -------------------------------- -------------------------------- 10325 Emp Client 0docHbkL8__7Ic58n7GCBA.. sql>
We then attempt to request an access token. Notice we are using the https://ol7-121.localdomain:8443/ords/pdb1/testuser1/oauth/auth?response_type=token&client_id=0docHbkL8__7Ic58n7GCBA..&state=3668D7A713E93372E0406A38A8C02171
BoxCLIENT_ID : 0docHbkL8__7Ic58n7GCBA..
State : 3668D7A713E93372E0406A38A8C02171
URL : https://ol7-121.localdomain:8443/ords/pdb1/testuser1/oauth/auth?response_type=code&client_id={client_id}&state={random-string}
https://ol7-121.localdomain:8443/ords/pdb1/testuser1/redirect#token_type=bearer&access_token=5SVR_NVP5N_OnDQt6iSxJg..&expires_in=3600&state=3668D7A713E93372E0406A38A8C02171
The application should check the state string matches the one used in the initial call. We can now access the protected resource using the access token.
$ curl -i -k -H"Authorization: Bearer 5SVR_NVP5N_OnDQt6iSxJg.." https://ol7-121.localdomain:8443/ords/pdb1/testuser1/testmodule1/emp/7788
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: "jtC17IXyetESUjSkxB2ani/a1TnFh28yfor+fLmxxUzGr6G9IFxQ77+/Gd71W4Qzz0rSxf90Qqbl+ICwezTayQ=="
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed,29 Jun 2016 12:15:35 GMT
{"items":[{"empno":7788,"deptno":20}]}
$
Deleting OAuth Metadata
We remove the OAuth Metadata for the client role mapping and the client using the following code. They could be combined as a single block,but I've left them separate so they can be run individually.
BEGIN OAUTH.revoke_client_role( p_client_name => 'Emp Client',p_role_name => 'emp_role' ); COMMIT; END; / BEGIN OAUTH.delete_client( p_name => 'Emp Client' ); COMMIT; END; /Deleting ORDS Metadata
The ORDS URL mapping,privilege and roles are deleted using the following code. Once this is removed,the web service will be publicly available again.
BEGIN ORDS.delete_privilege_mapping( p_privilege_name => 'emp_priv',p_pattern => '/testmodule1/emp/*' ); COMMIT; END; / BEGIN ORDS.delete_privilege ( p_name => 'emp_priv' ); COMMIT; END; / BEGIN ORDS.delete_role( p_role_name => 'emp_role' ); COMMIT; END; /