Mittwoch, 20. Januar 2016

ORDS und "3-Legged-OAuth": So geht's

Das erste Blog Posting des Jahres 2016 knüpft unmittelbar ans Jahr 2015 an. Zuletzt hatte ich vorgestellt, wie eine Authentifizierung für REST-Dienste mit ORDS eingerichtet werden kann. Zwei Verfahren wurden vorgestellt. Zunächst die einfache Variante, indem Username und Passwort als Basic-Authentication mit dem REST-Request mitgegeben werden und die erste OAuth-Variante, bei welcher sich die Applikation selbst am ORDS-Server anmeldet und ein Token abruft. Solange dieses Token gültig ist, müssen keine sensiblen Authentifizierungsdaten übertragen werden.

Heute geht es um das zweite OAuth-Verfahren, das sog. 3-Legged OAuth. Hier ist es so, dass sowohl die Applikation als auch der Endanwender authentifiziert werden. Für den Endanwender kommt hinzu, dass dieser seine (sensiblen) Login-Daten nicht etwa bei der Anwendung eingibt (wo diese komprimittiert werden könnten). Vielmehr loggt sich der Endanwender bei ORDS selbst ein und autorisiert den REST-Service. Dazu muss der Browser von der Anwendung auf die Login-Seite des ORDS und von dieser wieder zurück auf die Anwendung geleitet werden.

Wichtig für dieses Blog-Posting ist, dass Ihr mit der aktuellsen ORDS-Version 3.0.3 arbeitet. Die Version 3.0.2 enthält einen Fehler, so dass "3-Legged-OAuth" nicht läuft. Also am besten gleich auf 3.0.3 upgraden - das ist sehr einfach und schnell gemacht.

Im folgenden setzen wir also 3-Legged-OAuth für unseren REST-Service ein. Wir verwenden im wesentlichen die Definitionen vom letzten Blog Posting, der Vollständigkeit halber führe ich diese hier aber nochmals auf. Zuerst wird der REST-Service eingerichtet.

/*
 * Zuerst am Datenbankschema (hier: OAUTH3LEG) 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 =>    'oauth3leg.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
/

Nun schützen wir den REST-Service mit dem OAuth-Verfahren. Wie schon gesagt, melden sich Endanwender beim 3-Legged-OAuth-Verfahren direkt am ORDS an. Die User müssen dem Application Server, in dem der ORDS läuft, also bekannt sein. In produktiven Systemen wird es häufig so sein, dass LDAP-Server eingerichtet sind - Application Server wie Weblogic oder andere enthalten hierfür fertige Module. Endanwender können sich dann mit ihren Standard-Passwörtern anmelden. Für dieses Blog-Posting verwenden wir den Standalone-Modus von ORDS - Nutzerkonten werden darin wie folgt angelegt:

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

Legt nun einige User in eurem ORDS an - ich verwende e1, e2 und e3. Wichtig ist, dass Ihr die Rolle EmpViewer wie oben blau markiert zuweist. Nur User, welche diese Rolle haben, sollen des REST-Service nutzen können.

Danach legt Ihr die Rolle EmpViewer an und verknüpft sie mit dem REST-Service. Diese Zuordnung, dass die Rolle EmpViewer den REST Service /emp/list/* des Moduls oauth3leg.emp ausführen darf, merkt sich ORDS als Privileg. Das Privileg trägt den Namen my.listEmployees. Wichtig ist hier der Parameter P_DESCRIPTION, dieser muss gesetzt werden, nur dann erkennt ORDS, dass hier das 3-Legged-OAuth verwendet werden soll. Das ist auch nachvollziehbar, denn dieser Text wird dem Endanwender angezeigt, wenn er den Zugriff auf den REST-Service freigeben soll.

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

declare
  l_roles    owa.vc_arr;
  l_patterns owa.vc_arr;
  l_modules  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) := 'EmpViewer';

  -- Liste der URL-Patterns, die geschützt werden sollen.
  l_patterns(1) := '/emp/list/*';
  l_modules(1) := 'oauth3leg.emp';
  ords.define_privilege(
    p_privilege_name => 'my.listEmployees'
   ,p_roles =>          l_roles
   ,p_patterns =>       l_patterns     
   ,p_modules =>        l_modules      
   ,p_description =>    'Die Beschreibung ist wirklich wichtig!'
   ,p_label =>          'List of Employees'
  );
end;
/

commit
/

Num kommt die Einrichtung der Client-Anwendung - das sieht so ähnlich aus, wie beim letzten Blog-Posting, wir brauchen nur etwas andere Parameter: Nach erfolgreicher Anmeldung und Freigabe des REST-Requests muss der Browser ja zur Anwendung zurückgeleitet werden - diese Callback-URL muss angegeben werden. ORDS versieht diese dann mit Parametern und leitet den Browser zurück. Im Beispiel verwende ich eine statische Datei (http://localhost:8081/rest-oauth.html); es wäre aber auch ein komplett anderer Webserver oder gar eine spezielle URL zum Öffnen einer App auf einem mobilen Gerät denkbar. Außerdem wichtig ist der Parameter P_PRIVILEGE_NAMES, das legt fest, welche Privilegien (also Zuordungen von Rollen zu REST-Services) die Anwendung nutzen kann. Spielt also diesen PL/SQL Aufruf in euer Datenbankschema ein.

begin
 oauth.create_client(
    p_name => 'SimpleHtml'
   ,p_grant_type => 'authorization_code'
   ,p_owner => 'Carsten'
   ,p_description => 'Einfaches HTML / JS Beispiel'
   ,p_redirect_uri => 'http://localhost:8081/rest-oauth'
   ,p_support_email => 'carsten.czarski@oracle.com'
   ,p_support_uri => 'http://localhost:8081'
   ,p_privilege_names => 'my.listEmployees'
    );
end;
/
sho err

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 die nachfolgenden Schritte werden wir sie brauchen.

SQL> select name, client_id, client_secret from user_ords_clients;

NAME                           CLIENT_ID                        CLIENT_SECRET
------------------------------ -------------------------------- --------------------------------
SimpleHtml                     7NPUOs1pmKbgN6bL8jzQlw..         x7DC0U1vRG2X_mQzQCZExA..

1 Zeile wurde ausgewählt.

Nun wollen wir die OAuth-Authentifizierung testen. Der Setup ist nun etwas komplexer als beim letzten Mal und läuft in mehreren Schritten ab.

Zuerst braucht die Anwendung einen sog. Authentication Code - um diesen zu erhalten, muss der Endanwender aus der Anwendung heraus zu ORDS umgeleitet werden. Dort kann er sich anmelden und die gewünschte Operation freigeben. Diese Aktionen erfolgen mit dem Browser - der erste Schritt muss also mit einem Browser (oder einer HTML-Render-Bibliothek) erfolgen. Der Prozess zum Erlangen des Authentication Code sieht wie folgt aus:

  1. Die spezielle ORDS-URL /ords/{db-schema}/oauth/auth mit den URL-Paremetern response_type, client_id und state wird aufgerufen. response_type erhält den festen Wert "code", als client_id wird die CLIENT_ID aus der View USER_ORDS_CLIENTS mitgegeben und state sollte ein zufällig generierter Wert sein. Anhand des state kann die Client-Anwendung später erkennen, dass der Browser von ORDS zurückgeleitet wurde und sie kann es auf den richtigen Authentication-Code-Request matchen.
    /ords/oauth3leg/oauth/auth?response_type=code&client_id=7NPUOs1pmKbgN6bL8jzQlw..&state=4711
    
  2. Der Browser wird auf die URL aus dem ersten Schritt umgeleitet und fordert nun den Endanwender zum Anmelden auf.
  3. Der Endanwender meldet sich mit Usernamen und Passwort an - wohlgemerkt: Die Anwendung selbst bekommt die Login-Daten nicht zu sehen.
  4. Nach erfolgreicher Anmeldung zeigt ORDS, welche Rechte die Anwendung anfragt. Nun wird auch deutlich, warum die Beschreibung im Aufruf von ORDS.CREATE_PRIVILEGE so wichtig ist, denn diese dient der Erklärung des Vorgangs.
  5. Nach erfolgreicher Freigabe leitet ORDS den Browser auf die im Client konfigurierte Redirect-URL zurück. Der geforderte Authentication Code wird als URL-Parameter code angehängt. Außerdem wird der zufällig gewählte URL-Parameter state unverändert zurückgegeben - die Client-Anwendung kann also den Zusammenhang zum ersten Schritt herstellen.
    http://localhost:8081/rest-oauth?code=R8WkUCIQmxfq7ZqH3A3IGA..&state=4711
    
    Siehe dazu nochmals den Aufruf von ORDS.CREATE_CLIENT.
     oauth.create_client(
       :
      ,p_redirect_uri => 'http://localhost:8081/rest-oauth'
      ,p_support_email => ...
       :
     );
    

Als nächstes muss sich die Anwendung das Access Token von ORDS holen - dies wird dann wieder eine bestimmte Weile gültig sein und so das erneute Authentifizieren ersparen. Mit dem Authorization Code kann nur einmal ein Access Token angefordert werden; um ein neues Access Token zu erzeugen, braucht es einen neuen Authorization Code.

  1. Die Anwendung führt einen POST-Request auf die URL /ords/{db-schema}/oauth/token durch. Die URL erwartet eine HTTP Basic Authentication mit der CLIENT_ID aus der View USER_ORDS_CLIENTS als "Usernamen" und dem CLIENT_SECRET als "Passwort". Als Request-Body wird muss der vorher erlangte Authentication Code wie folgt übergeben werden.
    grant_type=authorization_code&code={Authentication Code aus Schritt 1}
    
    Verwendet man Javascript, jQuery und die Funktion ajax(), dann könnte der Call wie folgt aussehen.
      $.ajax({
        type: "POST",
        url: "/ords/oauth3leg/oauth/token",
        data: "grant_type=authorization_code&code=" + R8WkUCIQmxfq7ZqH3A3IGA..,
        beforeSend: function (xhr) {
          // HTTP Basic Auth mit CLIENT_ID und CLIENT_SECRET (siehe oben)
          xhr.setRequestHeader("Authorization", "Basic " + btoa("7NPUOs1pmKbgN6bL8jzQlw.." + ":" + "x7DC0U1vRG2X_mQzQCZExA.."));
        },
        success: function (data) {
          // process JSON here
        },
        dataType: "json"
      });
    
    Mit dem Kommandozeilentool curl sieht das so aus:
    C:\> curl -i 
                 --user 7NPUOs1pmKbgN6bL8jzQlw..:x7DC0U1vRG2X_mQzQCZExA..
                 --data "grant_type=authorization_code&code=R8WkUCIQmxfq7ZqH3A3IGA.." 
                 http://localhost:8081/ords/oauth3leg/oauth/token
    
  2. ORDS wird den Request im JSON-Format wie folgt beantworten.
    {
      "access_token":"EYrbSdTgG0IZ-QKc2g2OKQ..",
      "token_type":"bearer",
      "expires_in":3600,
      "refresh_token":"J9XgTEihshpobsUu3Ilncw.."
    }
    
Die Client-Anwendung kann also nicht nur das Access-Token selbst entnehmen, sondern auch die Gültigkeit in Sekunden und das Refresh-Token, mit dem sie sich ein neues Access Token ohne den gesamten Prozess holen kann. Mit dem Access Token kann nun auf den REST Service zugreifen; und das geht nicht nur mit dem Browser, sondern auch von anderen Prozessen aus, wie der folgende curl-Aufruf zeigt.

C:\> curl -H"Authorization: Bearer EYrbSdTgG0IZ-QKc2g2OKQ.." -i http://localhost:8081/ords/oauth3leg/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

So ist sehr gut nachvollziehbar, wie, zum Beispiel, eine mobile Anwendung mit ORDS kommuniziert. Der Endanwender öffnet die App (welche durchaus eine Native Anwendung sein kann); diese holt sich dann vom ORDS den Authentication Code. Auf dem mobilen Gerät öffnet sich dann der Browser, der Endbenutzer loggt sich ein und gibt den Request frei. Als Redirect-URL wird eine URL verwendet, welche wiederum die mobile Anwendung öffnet und dieser den Authentication Code übergibt. Nun kann die mobile Anwendung sich ein Access Token holen und die REST-Dienste aufrufen.

Viel Spaß beim Ausprobieren.

Kommentare:

  1. Hallo Carsten, super Anleitung! Klappt soweit prima, habe aber Probleme, das Token zu verlängern mit dem Refresh Token:


    curl --user "DCh0azAtvNX1TTbE76UXbQ..:Mr-pnJdV-XA4Y0DD05_BdQ.." --data "grant_type=authorization_code&code=sTXCxgn_7vZ80rrYrzBrhw.." http://vm1:8080/ords/ordstest/oauth/token

    liefert dann :

    DCh0azAtvNX1TTbE76UXbQ.. is authorized to access: oracle.dbtools.oauth.client.application



    Stack Trace

    oracle.dbtools.http.errors.InternalServerException: java.sql.SQLException: ORA-01403: no data found
    ORA-06512: at "ORDS_METADATA.OAUTH_INTERNAL", line 455
    ORA-06512: at "ORDS_METADATA.OAUTH", line 523
    ORA-06512: at line 1


    Klappt die "Verlängerung" bei Dir?

    Danke und Grüße,
    ~Dietmar.

    AntwortenLöschen
  2. Hi Dietmar,

    du hast zum Token Refresh den falschen Request abgesetzt: "grant_type=authorization_code" ist nur für das initiale Access Token vorgesehen. Den Token Refresh machst Du wie folgt (grant_type=refresh_token):

    curl -i --user DCh0azAtvNX1TTbE76UXbQ..:Mr-pnJdV-XA4Y0DD05_BdQ.. --data "grant_type=refresh_token&refresh_token={hier Dein Refresh Token}" http://vm1:8080/ords/ordstest/oauth/token

    In der Antwort bekommst Du ein neues Access Token und ein neues Refresh Token für das nächste Mal.

    Den "authorization code" kannst Du übrigens nur ein einziges Mal verwenden. Beim zweiten Versuch wird er und das darauf basierende Access Token nach der OAuth Spec invalidiert. Dann musst Du von vorne anfangen, also neue Authentifiuzierung, neuer Auth Code und neues Access Token. Damit genau das nicht passieren muss, gibt es das Refresh Token.

    Probier' mal ...

    Beste Grüße

    Carsten

    AntwortenLöschen
  3. Hallo Carsten, ja klappt jetzt super. Im Tutorial steht es falsch drin: https://docs.oracle.com/cd/E56351_01/doc.30/e56293/develop.htm#BABFGIFA

    woher hast Du denn die korrekte Syntax?

    Danke und Grüße,
    ~Dietmar.

    AntwortenLöschen
  4. Hier Dietmar,

    im "Image Gallery" Tutorial steht's richtig drin ...
    http://www.oracle.com/technetwork/developer-tools/rest-data-services/documentation/listener-dev-guide-1979546.html#extending_oauth_20_session_duration

    Beste Grüße

    Carsten

    AntwortenLöschen
  5. Hi Carsten,

    Interesting post. I have a question about securing REST-services. I want to consume a few REST-services from within an (native) app I developed. I read the documentation of Oracle and your blog and initially it works.
    However, after an hour (the default) access-token is invalidated. With the implicit grant I have to ask for a new token and my app has to be granted (manually) again. With the authorization-code I have a refresh-token and with one app/user it will work, but other users don't have that refresh-token. What is the way to go to secure ORDS-webservice? I'am retrieving data in the background, the users are already validated against database, but I don't want that every user has to 'approve' access (ords - login - approve client).

    AntwortenLöschen
  6. Hi Henk,
    sorry for the late answer. For background processes without human user interaction, the "client_credetials" flow (previous blog posting: "two-legged auth") is the appropriate one. First, you POST "grant_type=client_credentials" to the /oauth/token" url with the Client-ID and Client Secret as authentication pair. You'll get the access token back and use that for REST calls. That access token is valid for an hour and cannot be extended - so, after an hour you'll have to get a new access token. With the client credentials flow, you cannot have a refresh token.

    Hope this helps

    -Carsten

    AntwortenLöschen