Montag, 7. Dezember 2015

Bitte Anmelden: Authentifizierung für REST Dienste mit ORDS

In diesem Blog-Posting geht es wieder um Oracle REST Data Services (ORDS) - und hier um die Authentifizierung von Nutzern. Während dieses Thema bei allen anderen Postings in der Vergangenheit außen vor blieb, wollen wir es heute in den Mittelpunkt stellen. Dieses Blog-Posting wurde mit ORDS 3.0.2 erstellt.

ORDS bietet unterschiedliche Varianten an, REST Services mit einer Authentifizierung zu schützen. Der einfachste Weg sind Username und Passwort: etwas fortgeschrittener ist der Weg über das OAuth-Protokoll. Zunächst sei der Weg mit Username und Passwort kurz vorgestellt.

Um die Beispiele auszuprobieren, erzeugt Ihr am besten einen einfachen REST-Service wie folgt. Es wird angenommen, dass ORDS bereits installiert ist und läuft und dass Ihr Zugang zu einem Datenbankschema mit der EMP-Tabelle habt. Das Datenbankschema heißt in diesem Posting RESTAUTH; wenn euer Datenbankschema anders heißt, passt dies an den entsprechenden Stellen (vor allem ORDS-URLs) an. Wenn Ihr noch keine ORDS-Installation habt, findet Ihr Hinweise zur Installation in diesem Blog-Posting.

/*
 * Zuerst im Datenbankschema (hier: RESTAUTH) anmelden.
 * Datenbank-Schema für REST-Services freigeben, falls noch nicht geschehen.
 */

begin
  ords.enable_schema;
end;
/
sho err

commit
/

/*
 * ORDS Service erstellen - noch ohne Authentifizierung
 */

begin
  ords.define_service(
    p_module_name =>    'restauth.emp' ,
    p_base_path  =>     'emp/',
    p_pattern =>        'list/',
    p_method =>         'GET',
    p_source_type =>    ords.source_type_query,
    p_source =>         'select * from emp'
  );
end;
/
sho err

commit
/

Mit einem REST Client könnt Ihr den Service testen - und er wird funktionieren (im Moment noch ohne Authentifizierung). Hier arbeite ich mit dem Kommandozeilentool curl.

D:\> curl http://localhost:8081/ords/restauth/emp/list/ 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1845    0  1845    0     0   9088      0 --:--:-- --:--:-- --:--:--  9088
{
    "items" : [ {
        "empno" : 7369,
        "ename" : "SMITH",
        "job" : "CLERK",
        "mgr" : 7902,
        "hiredate" : "1980-12-16T23:00:00Z",
        "sal" : 800,
        "comm" : null,
        "deptno" : 20
    }, {
        "empno" : 7499,
        "ename" : "ALLEN",
        "job" : "SALESMAN",
        "mgr" : 7698,

Sicherheit - Schritt 1: Usernamen und Passwort

Nun geht es daran, diesen REST-Service zu schützen. ORDS arbeitet zunächst mit Privilegien und Rollen. Diese werden im zweiten Schritt dann an Usernamen (bei Username/Passwort-Authentifizierung) oder an registierte OAuth-Clients vergeben. Die folgenden PL/SQL Aufrufe erzeugen ein Privileg listEmployees und eine Rolle EmpAdmin.

begin
  ords.create_role(
    p_role_name => 'EmpAdmin'
  );
end;
/
sho err

declare
  l_roles    owa.vc_arr;
  l_patterns owa.vc_arr;
begin
  -- Liste der Rollen, denen das Privileg zugeordnet sein soll 
  -- Weitere werden mit l_roles(2), l_roles(3) usw. hinzugefügt.
  l_roles(1) := 'EmpAdmin';

  -- Liste der URL-Patterns, die geschützt werden sollen.
  l_patterns(1) := '/emp/list/*';

  ords.define_privilege(
    p_privilege_name => 'listEmployees'
   ,p_roles =>          l_roles
   ,p_patterns =>       l_patterns     
   ,p_label =>          'List of Employees'
  );
end;
/

commit
/

Nun sind Rolle und Privileg angelegt; und alle URL, die mit /emp/list beginnen, sind geschützt. Versucht man den curl-Aufruf von vorhin nochmal, so bekommt man nun einen Fehler.

HTTP/1.1 401 Unauthorized
Content-Type: text/html
Content-Length: 11549

:

Im Browser würde man das folgende Bild sehen.

Doch wie meldet man sich an? Im ORDS wurden nur Rollen und Privilegien definiert, aber noch keine Usernamen und Passwörter. Grund ist, dass ORDS selbst keine Benutzernamen und Passwörter verwaltet - diese Aufgabe übernimmt der Application Server, in dem ORDS läuft. Verwendet man einen Tomcat, so müssen nun im Tomcat die Usernamen und Passwörter angelegt werden, gleiches gilt für Weblogic oder jeden anderen Java-Server.

Auch der Standalone-Webserver, mit dem ORDS ausgeliefert ist, bringt eine minimalistische Nutzerverwaltung mit. Darin legen wir nun einen neuen User (rot) an und weisen diesem gleichzeitig die Rolle EmpAdmin (blau) zu.

C:\> java -jar ords.war user cczarski "EmpAdmin"
Enter a password for user cczarski: ********
Confirm password for user cczarski: ********
Dez 07, 2015 10:22:32 AM oracle.dbtools.standalone.ModifyUser execute
INFO: Created user: cczarski in file: D:\oracle\ordsconf\ords\credentials
C:\>

Um den Service nun (wiederum mit curl) aufrufen zu können, müssen Authentifizierungsdaten mitgegeben werden (HTTP Basic Authentication).

D:\> curl -u cczarski:******** -i http://localhost:8081/ords/restauth/emp/list/
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "AwR8DXNHtIkyk4PbmSr0PJnZbaCYETdkmg17Yi28Q3dKq4a6+De3o/GMEbQiv+TjY5z/Owyr3hnHfk2RPmd7Pg=="
Transfer-Encoding: chunked

{
    "items" : [ {
        "empno" : 7369,
        "ename" : "SMITH",
        "job" : "CLERK",
        "mgr" : 7902,
        "hiredate" : "1980-12-16T23:00:00Z",
        "sal" : 800,
        "comm" : null,
        "deptno" : 20

Wichtig bei diesem Setup ist, dass alles genau zusammenpasst: Die Rolle, die dem Nutzerkonto im Application Server zugewiesen ist, muss genau zur Rolle, die mit ORDS.CREATE_ROLE erzeugt wurde, passen. Auch Groß- und Kleinschreibung sind hier wichtig. Zu empfehlen ist auch, sich auf reine ASCII-Zeichen zu beschränken und das Leerzeichen nicht zu nutzen. Es mag zwar sein, dass ORDS damit keine Probleme hat - es kann aber sein, dass es Beschränkungen in den Java-Containern gibt, die dann dazu führen, dass die Rollendefinitionen nicht mehr zusammenpassen.

Mehr Sicherheit: OAuth-Verfahren

Der größte Nachteil am bis jetzt beschriebenen Verfahren ist, dass die Login-Credentials (Usernamen und Passwort) bei jedem Request mitgeschickt werden müssen. Technisch wird das Format {username}:{password} bei dieser Basic-Authentication ins Base64-Format konvertiert und als HTTP-Header Authorization mitgesendet. Das Verwenden von HTTPS sollte bei dieser Authentifizierungsvariante selbstverständlich sein - ohne SSL-Verschlüsselung kann jedermann die Login-Daten mitlesen. Aber auch mit Verschlüsselung gibt es bessere Verfahren: So wäre es doch sinnvoller, Usernamen und Passwort nur einmal (bei der intialen Anmeldung) zu senden und danach mit einem Token zu arbeiten. Das hat den entscheidenden Vorteil, dass auch die Client-Anwendung sich das Passwort nicht meht mehr merken muss. Im folgenden setzen wir also eine einfache OAuth-Authentifizierung für unseren REST-Service auf.

Ein OAuth-Client muss zuerst als solcher registriert werden. Das geht wie folgt:

/*
 * Client-Applikation erstellen
 */

begin 
  oauth.create_client(
    p_name =>            'OAuth EMP List Application',
    p_grant_type =>      'client_credentials',
    p_privilege_names => '',
    p_support_email =>   'carsten.czarski@oracle.com'
  );
end;
/
sho err

commit
/

/*
 * Rolle zuweisen
 */

begin
  oauth.grant_client_role(
    p_client_name => 'OAuth EMP List Application',
    p_role_name =>   'EmpAdmin'
  );
end;
/

commit 
/

Nun ist die Applikation registriert und ORDS hat eine Client-ID und ein Client-Secret erzeugt. Diese kann man sich als Usernamen und Passwort für eine Client-Anwendung vorstellen. Die folgende SQL-Abfrage zeigt sie an - für den nachfolgenden curl Aufruf werden wir sie brauchen.

SQL> select name, client_id, client_secret from user_ords_clients;

NAME                           CLIENT_ID                        CLIENT_SECRET
------------------------------ -------------------------------- --------------------------------
OAuth EMP List Application     oMQvSnxOUOTXWd3oKgZN9Q..         PyFS4qVFSe_hk08BzTlmtA..

1 Zeile wurde ausgewählt.

Auch die Zuordnung der Rolle zur Client-Anwendung kann man sich ansehen.

SQL> select * from user_ords_client_roles;

 CLIENT_ID CLIENT_NAME                       ROLE_ID ROLE_NAME
---------- ------------------------------ ---------- ---------------
     10508 OAuth EMP List Application          10489 EmpAdmin

1 Zeile wurde ausgewählt.

Nun wollen wir die OAuth-Authentifizierung testen. Dazu braucht es im ersten Schritt die oben angezeigten Client-ID und das Client-Secret (bei euch sind das natürlich dann andere Werte). Der erste REST-Call mit curl dient nun allein der Authentifizierung der Client-Anwendung - und wir bekommen demzufolge auch nur ein Access Token zurück. Mit diesem führt man danach den eigentlichen REST-Call durch. Zuerst also der Call zur Authentifizierung ...

C:\> curl -i 
             --user oMQvSnxOUOTXWd3oKgZN9Q..:PyFS4qVFSe_hk08BzTlmtA.. 
             --data "grant_type=client_credentials" 
             http://localhost:8081/ords/restauth/oauth/token
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked

{"access_token":"LfXJilIBdzj5JPRn4xb5QQ..","token_type":"bearer","expires_in":3600}

Der OAuth-Dienst von ORDS steht unter dem URL-Pattern oauth/token bereit. Client-ID und Client-Secret werden wie als HTTP Basic Authentication mitgegeben. In einem REST-Client im Browser sieht das in etwa wie folgt aus.

In der JSON-Antwort findet sich nun das Access-Token, dass bei fortfolgenden REST-Anfragen wie folgt als HTTP-Header Authorization mitgegeben wird.

C:\> curl -H"Authorization: Bearer nHpPl5J_0NgtFephW7n6gQ.." -i http://localhost:8081/ords/restauth/emp/list/
 
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "AwR8DXNHtIkyk4PbmSr0PJnZbaCYETdkmg17Yi28Q3dKq4a6+De3o/GMEbQiv+TjY5z/Owyr3hnHfk2RPmd7Pg=="
Transfer-Encoding: chunked

{
    "items" : [ {
        "empno" : 7369,
        "ename" : "SMITH",
        "job" : "CLERK",
        "mgr" : 7902,
        "hiredate" : "1980-12-16T23:00:00Z",
        "sal" : 800,
        "comm" : null,
        "deptno" : 20

Das Token ist 3600 Sekunden, also eine Stunde lang gültig. Solange müssen Client-ID und Client-Secret nicht mehr über die Leitung gesendet werden. Das reicht typischerweise für eine Anwendungssession aus.

Das OAuth-Protokoll geht aber noch weiter: Das Ziel ist es, dass eine externe Anwendung (losgelöst von ORDS) eine OAuth-Authentifizierung startet - woraufhin der Browser zur Anmeldemaske von ORDS geleitet wird. Nach erfolgreicher Anmeldung wird der Browser zur externen Anwendung zurückgeleitet und das OAuth-Token wird dabei mitgesendet. Die externe Anwendung kann mit diesem Token nun REST-Dienste im ORDS aufrufen. Die Anwendung muss sich keinerlei Usernamen und Passwörter merken und kennt allein die OAuth-Tokens. Der Endanwender gibt seine Usernamen und Passwörter nur in die Anmeldemaske des ORDS ein. Wie das aufgesetzt wird, wird dann in einem der nächsten Blog-Postings erläutert. Bis dahin viel Spaß beim Ausprobieren.

Kommentare:

  1. Hi, we are developing two mobile apps using framework ionic, we have posted theses apps to apple store and play store already, the thing is we had to create our own mechanism to generate tokens and its really a pain, only because we could not figure out how to use Oauth authentication.

    We have ORDS 3.02 installed, oracle 12c and APEX 5. I use postman to try all the API's (posts/gets/puts) and we would really appreciate you talking more about it.

    Here is our scenario:

    User downloads the app, create a user name and password and then get access to the app, inside the app is all about API's. Two things: How we get the token and pass the token on each rest request? I need to let the user signed in for as much as he like, how to do it?

    Thank you

    AntwortenLöschen
  2. Hi Andre,

    to make sure that I understand your situation correctly: You have to build your own OAuth implementation (server-side) or can you use the OAuth Implementation of ORDS ...?

    Regarding of individual Usernames and Passwords: ORDS is also capable to do the "three-legged" OAuth flow. That means, that a user opens the app - and gets redirected from the App to ORDS in order to log in. After login, the user can decide whether to approve the app's access request or not. Upon approval, ORDS sends a token to the app which the app then can convert to an access token in order to do REST requests.

    Best regards

    -Carsten

    AntwortenLöschen