Logo Oracle Deutschland   APEX und PL/SQL Community
APEX-Nutzer "woanders" authentifizieren: Authentication Plugin am Beispiel "Cookie"
Erscheinungsmonat APEX-Version Datenbankversion
Juli 2012 ab 4.1 ab 10.2

Wenn APEX-Anwendungen in eine IT-Landschaft eingebettet werden sollen, reicht die APEX-Workspace-Authentifizierung vielfach nicht mehr aus. In vielen Fällen wird dann die Authentifizierung mit einem LDAP-Server verwendet. Hierfür bringt APEX zwar ein fertiges Authentifizierungsschema mit, allerdings erfordert dieses immer noch die Eingabe des LDAP-Passworts auf der APEX-Anmeldeseite. Oft ist jedoch ein Single-Sign-On (SSO) gewünscht - die Nutzer melden sich also einmal an einem Server an und diese Anmeldung wird auch für die APEX-Anwendungen verwendet.

Das Besondere ist nun, dass das Darstellen der Anmeldeseite, die Entgegennahme von Nutzername und Passwort und deren Überprüfung des Passworts außerhalb von APEX erfolgt - die Anmeldeseite 101 hat keine Bedeutung mehr. Für bestimmte Umgebungen, wie bspw. den Oracle Single-Sign-On-Server bringt APEX bereits fertige Authentifizierungsschemas mit.

Dieser Tipp stellt dagegen vor, wie man, mit einem Plugin, eine externe Authentifizierung in seine APEX-Applikation einbinden kann. Der externe "Login-Server" ist in diesem Beispiel selbst gebaut und sehr simpel. Die Einbindung in die APEX-Anwendung kann allerdings exemplarisch für alle anderen Login-Server angesehen werden - oder mit anderen Worten: Auf diese Art und Weise dürfte sich nahezu jeder Login-Server mit APEX nutzen lassen.

Bitte beachten Sie, dass der Code dieses Tipps als Beispielcode zu verstehen ist. Er soll dabei helfen, die Vorgehensweise zur Erstellung von Authentifizierungsplugins zu erläutern. Auf keinen Fall ist er ohne weiteres zum produktiven Einsatz geeignet - es gelten die Terms Of Use des Oracle Technology Network für Beispielcode.

Als "externen Login-Server" nehmen wir zwei PL/SQL-Prozeduren, die mit dem Paket HTP, also wie ein Java-Servlet, arbeiten. So brauchen Sie zum Ausprobieren keinen zusätzlichen Server. Die Prozeduren werden direkt, also an APEX vorbei, aufgerufen - das wirkt genauso, als ob es ein externer Server, PHP, Java, .NET oder etwas völlig anderes wäre. Ein Login wird in diesem Beispiel nach dem in Abbildung 1 skizzierten Schema ablaufen.

Ablaufdiagramm eines "selbstgebauten" Login-Verfahrens

Abbildung 1: Ablaufdiagramm eines "selbstgebauten" Login-Verfahrens

Login-Server aufsetzen

Spielen Sie mit SQL*Plus oder dem SQL Workshop das folgende SQL-Skript ins Parsing Schema Ihrer Anwendung ein. Es erzeugt die PL/SQL-Prozeduren LOGIN_PAGE und DO_LOGIN. Beide werden, wie bereits erwähnt, direkt und an APEX vorbei aufgerufen. Wichtig ist daher die Vergabe des EXECUTE-Privilegs an PUBLIC ganz unten im Skript.

Achten Sie darauf, den Parameter DOMAIN im Aufruf von OWA_COOKIE.SEND an Ihre Umgebung anzupassen. Dieser Parameter legt fest, an welche Server der Browser das Cookie MY_REMOTE_USER senden darf. Er muss so eingestellt werden, dass die APEX-Instanz das Cookie auch bekommt.

set define off

create or replace procedure login_page(
  p_apex_session in varchar2,
  p_apex_app     in varchar2,
  p_apex_page    in varchar2,
  p_apex_url     in varchar2,
  p_msg          in varchar default null
) is 
begin
  htp.p('<html>');
  htp.p('<body>');
  htp.p('<form action="'||sys_context('userenv','current_schema')||'.do_login" method="post">');
  htp.p('<input type="hidden" name="p_apex_session" value="'||p_apex_session||'">');
  htp.p('<input type="hidden" name="p_apex_app"     value="'||p_apex_app    ||'">');
  htp.p('<input type="hidden" name="p_apex_page"    value="'||p_apex_page   ||'">');
  htp.p('<input type="hidden" name="p_apex_url"     value="'||p_apex_url    ||'">');
  htp.p('<h1>Login</h1>');
  htp.p('<br/>');
  htp.p('<h3 style="color:red">'||p_msg||'</h3>');
  htp.p('<table>');
  htp.p('<tr>');
  htp.p('<td>Username:</td><td><input size="30" name="p_username"/></td>');
  htp.p('</tr>');
  htp.p('<tr>');
  htp.p('<td>Passwort:</td><td><input type="password" size="30" name="p_password"/></td>');
  htp.p('</tr>');
  htp.p('</table>');
  htp.p('<br/>');
  htp.p('<br/>');
  htp.p('<input type="submit" value="LOGIN">');
  htp.p('</form>');
  htp.p('</body>');
  htp.p('</html>');
end login_page;
/
sho err

create or replace procedure do_login(
  p_apex_session in varchar2,
  p_apex_app     in varchar2,
  p_apex_page    in varchar2,
  p_apex_url     in varchar2,
  p_username     in varchar2,
  p_password     in varchar2
) is 
begin
  if p_password = 'oracle' then 
    owa_cookie.send(
      NAME     => 'MY_REMOTE_USER',
      VALUE    => p_username,
      EXPIRES  => null,
      PATH     => '/',
      -- Diesen Parameter an die APEX-Umgebung anpassen
      DOMAIN   => '.domain.meinefirma.de',
      SECURE   => null
    );
    owa_util.redirect_url(
      p_apex_url || '/f?p='||p_apex_app||':'||p_apex_page||':'||p_apex_session
    );
  else 
    owa_util.redirect_url(
      sys_context('userenv','current_schema')||'.login_page'||
      '?p_apex_session='||p_apex_session||
      '&p_apex_app='||p_apex_app||
      '&p_apex_page='||p_apex_page||
      '&p_apex_url='||p_apex_url||
      '&p_msg=Fehlerhafter Login!'
    );
  end if;
end do_login;
/
sho err

grant execute on login_page to public
/
grant execute on do_login to public
/

Die Prozedur LOGIN_PAGE stellt einfach nur eine einfache Login-Seite bereit. Beachten Sie die Parameter der Prozedur - sie muss bereits mit Informationen über die APEX-Anwendung und die APEX-Session aufgerufen werden; diese Informationen braucht sie zwar selbst nicht, sie werden aber an die nachfolgende Prozedur DO_LOGIN durchgereicht. DO_LOGIN prüft zunächst, ob das Passwort gleich oracle ist - wenn ja, wird der Username in ein Cookie (MY_REMOTE_USER) geschrieben und der Browser zur APEX-Anwendung zurückverwiesen. Stimmt das Passwort nicht, wird auf die Login-Seite zurückverwiesen.

Natürlich ist es nicht besonders sicher, den Usernamen direkt ins Cookie zu schreiben - aber hier geht es erstmal um die generelle Vorgehensweise. Im nächsten Schritt (weiter unten) werden wir das Verfahren dann sicherer machen und nur noch ein "Session-Token" ins Cookie schreiben.

Den "externen Login-Server" in die APEX-Anwendung einbinden

Erstellen Sie zunächst eine einfache APEX-Anwendung mit nur einer Seite (die Instant Application reicht aus) - das Authentifizierungsverfahren ist erstmal egal. Um den Erfolg der Integration prüfen zu können, fügen Sie der Anwendungsseite eine HTML-Region mit folgender Regionsquelle hinzu:

Angemeldet als "&APP_USER."

Wenn Sie diese Anwendung zum ersten Mal starten, sollte das in etwa wie in Abbildung 2 aussehen (welcher Username tatsächlich angezeigt wird, hängt natürlich vom verwendeten Login-Verfahren ab).

APEX-Anwendung zum Testen

Abbildung 2: APEX-Anwendung zum Testen

Die Einbindung unseres "externen Login-Servers" implementieren wir direkt als Plugin, denn seit APEX 4.1 kann man auch Authentifizierungsschemas als Plugin erstellen. Für dieses Beispiel haben wir das Plugin bereits vorbereitet und Sie können es herunterladen und direkt als Plugin importieren. Navigieren Sie also zu den Gemeinsamen Komponenten, dort zu den Plugins. Klicken Sie dann auf Importieren - darauf sollten Sie den in Abbildung 3 dargestellten Dialog sehen.

Authentifizierungs-Plugin importieren

Abbildung 3: Authentifizierungs-Plugin importieren

Nach dem Import sehen Sie den folgenden Dialog mit den Details zum Plugin. Wie immer bei APEX Plugins, können Sie auch dieses Plugin nach Belieben verändern oder erweitern (Abbildung 4).

Das Authentifizierungs-Plugin wurde importiert

Abbildung 4: Das Authentifizierungs-Plugin wurde importiert

Grundlage für unser Plugin ist eine Forums-Diskussion, in der Patrick Wolf den Code des in APEX eingebauten Authentifizierungsschemas HTTP Header veröffentlicht hat. Diesen Code könnte man nun so ändern, dass anstelle einer HTTP-Header-Variablen ein Cookie ausgelesen wird. Des weiteren ist in dem Forums-Thread auch die grundsätzliche Abfolge einer Authentifizierung erklärt ...

  • Zuerst wird die Session-Sentry-Funktion aufgerufen. Deren Aufgabe ist es, festzustellen, ob die aktuelle Session gültig ist oder nicht - dementsprechend gibt sie auch nur true oder false zurück. In unserem Beispiel soll sie also prüfen, ob überhaupt schon eine APEX-Session und ein Username vorliegt und ob er zu den Angaben im Cookie des "externen Login-Servers" passt. Wenn die Session gültig ist, liefert APEX die gewünschte Seite aus, ansonsten wird zur Ungültige-Session-Funktion verzweigt.
  • Die Ungültige-Session-Funktion prüft nun, ob ein Cookie vom "externen Login Server" vorliegt und ob es in Ordnung ist. Wenn ja, wird eine APEX Session aufgebaut und mit APEX_AUTHENTICATION.LOGIN "registriert". Damit liegt eine gültige Session vor und APEX kann die Anwendungsseite ausliefern.
  • Die AJAX-Funktion ist für den externen Login-Server vorgesehen: Wenn dieser von sich aus (nach erfolgreichem Login) die Funktion APEX_AUTHENTICATION.CALLBACK aufruft, wird der Code der AJAX-Funktion aufgerufen. In unserem Beispiel verwenden wir sie nicht.
  • Die Authentifizierungsfunktion wird genutzt, wenn das Login auf einer APEX-Seite (bspw. Seite 101) und auch die Passwortprüfung in APEX erfolgt. Dann wird hier der Code zum Prüfen des Passworts hinterlegt.
  • In der Post-Abmeldefunktion kann zusätzlicher Code zum Aufräumen von Session-Informationen eingetragen werden. Auch diese verwenden wir in unserem Beispiel nicht.

Im Bereich PL/SQL-Code des Plugins ist demnach der folgende Code hinterlegt.

--==============================================================================
-- Helper functions to read the value from a cookie
--==============================================================================
function read_value_from_cookie(p_cookie_name in varchar2) return varchar2 is
  l_cookie_value varchar2(255);
  l_cookie       owa_cookie.cookie;
begin
  l_cookie := owa_cookie.get(p_cookie_name);
  if l_cookie.num_vals > 0 then 
    l_cookie_value := l_cookie.vals(1);
  else
    l_cookie_value := null;
  end if;
  return upper(l_cookie_value); 
end read_value_from_cookie;

function convert_value_using_plsql(p_plsql_func in varchar2, p_cookie_value in varchar2) return varchar2 is
  l_converted_value varchar2(255);
begin
 begin
   execute immediate 'begin :conv_val := upper('||p_plsql_func||'(:username)); end;' using out l_converted_value, in p_cookie_value; 
 exception
   when NO_DATA_FOUND then
     l_converted_value := null;
   when others then 
     raise;
 end;
 return l_converted_value;
end convert_value_using_plsql;

--==============================================================================
-- Session Sentry function for COOKIE_BASED authentication.
--==============================================================================
function cookie_based_session_sentry (
    p_authentication in apex_plugin.t_authentication,
    p_plugin         in apex_plugin.t_plugin,
    p_is_public_page in boolean )
    return apex_plugin.t_authentication_sentry_result
is
    c_cookie_name  constant varchar2(255) := nvl(p_authentication.attribute_01, 'COOKIE_USER');
    c_plsql_func   constant varchar2(255) := p_authentication.attribute_05;
    c_verify       constant varchar2(15)  := nvl(p_authentication.attribute_06, 'ALWAYS');
 
    l_username     varchar2(255);
    l_result apex_plugin.t_authentication_sentry_result;
begin
    if c_verify = 'ALWAYS' then
      l_username     := read_value_from_cookie(c_cookie_name);

      wwv_flow.debug('Username in Cookie '||c_cookie_name||' = '||l_username);
 
      if c_plsql_func is not null then
        l_username := convert_value_using_plsql(c_plsql_func, l_username);
        wwv_flow.debug('Username from PL/SQL function '||c_plsql_func||' = '||l_username);
      end if;
    end if;


    -- If we have a public page or a
    -- valid session (stored username equals username in CGI variable)
    -- we are done, otherwise we are returning FALSE so that our
    -- invalid session function is called which registers the username in the session
    -- We also check apex_application.g_user equals l_username, because in case if
    -- it's a public user, p_authentication.username will always return null and
    -- we want to avoid a reauthentication of public user each time a page is requested.
    --
    -- Note: Only if verify username is set to ALWAYS we make sure that the username
    --       stored in our APEX session is still the same what we have in the CGI variable
    --       Some security servers might just set the CGI variable after login or just
    --       for a specific URL, that's why we can't always rely on that value.
 
    if p_is_public_page then
        l_result.is_valid := true;
    elsif c_verify = 'ALWAYS' then
        l_result.is_valid := case
                               when upper(p_authentication.username) = l_username then true
                               when upper(apex_application.g_user)   = l_username then true
                               else false
                             end;
    elsif c_verify = 'CALLBACK' then
        l_result.is_valid := case
                               when p_authentication.username is not null then true
                               when apex_application.g_user   is not null then true
                               else false
                             end;
    else
        l_result.is_valid := true;
    end if;
 
    return l_result;
 
end cookie_based_session_sentry;
--
--
--==============================================================================
-- Invalid Session function for COOKIE_BASED authentication.
--==============================================================================
function cookie_based_invalid_session (
    p_authentication in apex_plugin.t_authentication,
    p_plugin         in apex_plugin.t_plugin )
    return apex_plugin.t_authentication_inval_result
is
    c_cookie_name  constant varchar2(255)  := nvl(p_authentication.attribute_01, 'COOKIE_USER');
    c_action        constant varchar2(15)   := nvl(p_authentication.attribute_02, 'BUILTIN_URL');
    c_url           constant varchar2(4000) := p_authentication.attribute_03;
    c_error_message constant varchar2(4000) := p_authentication.attribute_04;
    c_plsql_func    constant varchar2(255)  := p_authentication.attribute_05;
    c_verify        constant varchar2(15)   := nvl(p_authentication.attribute_06, 'ALWAYS');
 
    l_username      varchar2(255);
    l_result        apex_plugin.t_authentication_inval_result;
begin
    l_username     := read_value_from_cookie(c_cookie_name);

    wwv_flow.debug('Username in Cookie '||c_cookie_name||' = '||l_username);
 
    if c_plsql_func is not null then
      l_username := convert_value_using_plsql(c_plsql_func, l_username);
      wwv_flow.debug('Username from PL/SQL function '||c_plsql_func||' = '||l_username);
    end if;
 
    if  l_username is not null then
        -- Register the current database user in the session
        apex_authentication.login (
            p_username           => l_username,
            p_password           => null,
            p_uppercase_username => true );
 
        -- Session has successfully been registered, redirect has already been done by login,
        -- so it's not necessary to set redirect_url.
    else
        -- No username has been set in the CGI variable, we have to trigger that by
        -- redirecting to an URL so that the web server sets it.
        -- -) BUILTIN_URL redirects to /xxx/apex_authentication.callback which can
        --                be checked by the web server to initiate a login.
        -- -) URL         redirects to an external URL for sign-in and then calls
        --                our callback procedure to actually login.
        -- -) ERROR       just shows an error message if the username is not set.
        --
        case c_action
          when 'BUILTIN_URL' then
              l_result.redirect_url := apex_authentication.get_callback_url;
          when 'URL' then
              l_result.redirect_url := c_url;
          when 'ERROR' then
              raise_application_error(-20000, c_error_message);
        end case;
    end if;
 
    return l_result;
end cookie_based_invalid_session;

Die im Code enthaltenen Funktionen sind entsprechend bei Session-Sentry-Funktionsname und Ungültiger Sessionfunktionsname eingetragen (Abbildung 5).

Im Plugin verwendete PL/SQL-Funktionen und PL/SQL Code

Abbildung 5: Im Plugin verwendete PL/SQL-Funktionen und PL/SQL Code

Im Plugin-Code sind keine hart kodierten Werte vorhanden; schließlich soll es auch mit JSPs, PHP oder anderen "externen Login-Servern" zusammenarbeiten können. Daher wird das Plugin parametrisiert - scrollen Sie herunter zum Abschnitt Attribute (Abbildung 6).

Attribute des Authentifizerungsplugins

Abbildung 6: Attribute des Authentifizerungsplugins

Sie können das Plugin damit speichern und verlassen - hier ist nichts weiter zu tun. Nun wollen wir das Plugin in der Anwendung nutzen. Dazu navigieren Sie nochmals zu den Gemeinsamen Komponenten und dort zu den Authentifizierungsschemas (Abbildung 7).

In der Anwendung vorhandene Authentifizierungsschemas

Abbildung 7: In der Anwendung vorhandene Authentifizierungsschemas

Mit Klick auf Erstellen erzeugen Sie ein neues Schema. Im darauf folgenden Dialog wählen Sie Basiert auf einem vorkonfigurierten Schema aus der Galerie aus und klicken auf Weiter. Sie sehen daraufhin den in Abbildung 8 gezeigten Dialog.

Authentifizierungstyp auswählen

Abbildung 8: Authentifizierungstyp auswählen

Neben den mit APEX standardmäßig ausgelieferten Authentifizierungstypen ist nun auch unser Plugin Cookie-Based Authentication dabei. Wählen Sie es aus ...

Plugin Parameter einstellen

Abbildung 9: Plugin Parameter einstellen

... und Sie sehen die Parameter des Plugins. Stellen Sie diese wie folgt ein.

  • Der Cookie-Name ist MY_REMOTE_USER
  • Bei fehlendem Cookie leiten Sie auf Eigene URL um.
  • Die URL bei fehlerhaftem Cookie sollte nun auf die Login-Seite unseres zu Beginn dieses Tipps erstellten "externen Login-Servers" zeigen. In unserem Beispiel ist das also die PL/SQL-Prozedur LOGIN_PAGE. Geben Sie alle Parameter mit, die diese Seite erwartet - und die URL muss natürlich in einer Zeile sein.
    http://{host}:{port}/apex/{schema}.LOGIN_PAGE?
      P_APEX_SESSION=&APP_SESSION.&
      P_APEX_APP=&APP_ID.&
      P_APEX_PAGE=1&
      P_APEX_URL=http://{host}:{port}/apex
    
    Konkret könnte das so aussehen:
    http://sccloud030.de.oracle.com:8080/apex/TESTIT.LOGIN_PAGE?
      P_APEX_SESSION=&APP_SESSION.&
      P_APEX_APP=&APP_ID.&
      P_APEX_PAGE=1&
      P_APEX_URL=http://sccloud030.de.oracle.com:8080/apex
    
  • Die PL/SQL-Funktion lassen Sie (noch) leer - denn (noch) steht der Username im Klartext im Cookie ...
  • Schließlich soll das Cookie immer geprüft werden.

Nach dem Speichern ist das neue Authentifizierungsschema sofort aktiv (Abbildung 10).

Das neue Authentifizierungsschema ist nun aktiv

Abbildung 10: Das neue Authentifizierungsschema ist nun aktiv

Nun können Sie testen - starten Sie Ihre Anwendung. Sie sollten auf die PL/SQL-Prozedur LOGIN_PAGE verzweigt werden (Abbildung 11). Beachten Sie, dass Sie hier außerhalb des APEX-Kontexts sind - hier wäre, wie schon gesagt, auch eine PHP-Seite oder eine Java-Applikation denkbar.

Bei Start der Anwendung sehen Sie die Login-Seite

Abbildung 11: Bei Start der Anwendung sehen Sie die Login-Seite

Loggen Sie sich mit irgendeinem Namen und dem Passwort oracle ein. Daraufhin wird das Cookie gesetzt und Sie werden an APEX zurückverzweigt. APEX liest den Usernamen aus dem Cookie aus und baut die Session damit auf. Abbildung 12 zeigt die gleiche Anwendung wie Abbildung 2 zu Beginn, nun ist die Session allerdings mit dem Usernamen aus dem Cookie aufgebaut.

Die APEX-Session wurde mit dem Usernamen aus dem Cookie aufgebaut

Abbildung 12: Die APEX-Session wurde mit dem Usernamen aus dem Cookie aufgebaut

Sie können sich nun in der APEX-Anwendung bewegen, die APEX-Session bleibt erhalten. Selbst wenn Sie die Session-ID aus der URL entfernen und die Seite neu laden (normalerweise würden Sie also ein neues Login-Fenster sehen), bleibt die Session erhalten (denn das Cookie des "externen Login-Servers" ist ja noch da. In den Browser-Einstellungen, im Bereich Cookies, können Sie das Cookie übrigens auch sehen - suchen Sie nach einem Cookie namens MY_REMOTE_USER. Im in Abbildung 13 dargestellten Cookie hat sich jemand mit dem Benutzernamen test angemeldet.

Das Cookie MY_REMOTE_USER kann betrachtet werden

Abbildung 13: Das Cookie MY_REMOTE_USER kann betrachtet werden

Das Cookie-Verfahren sicherer machen

Spätestens beim direkten Betrachten des Cookies wird die Schwachstelle dieses Ansatzes deutlich. Wenn man sich das Cookie im Browser einfach so ansehen kann, dann kann man es auch manipulieren. Man könnte seinen Browser also dazu bringen, dass er die APEX-Seite, mit einem Cookie namens MY_REMOTE_USER und gewünschten Inhalt, direkt aufruft. APEX würde das Cookie erkennen und die Session aufbauen. Als erster Schritt war das also ganz gut - es reicht aber nicht aus. Eine Variante wäre nun, den Usernamen im Cookie zu verschlüsseln - wir wollen hier aber noch etwas weiter gehen. Dazu müssen wir zuerst einen etwas intelligenteren "externen Login-Server" schaffen.

Spielen Sie, wie zu Beginn dieses Tipps, mit SQL*Plus oder dem SQL Workshop das folgende Skript im Parsing Schema Ihrer APEX-Anwendung ein. Die Prozedur DO_LOGIN wird nun ausgetauscht.

set define off

create table  my_sessions (
  sid          number primary key,
  username     varchar2(200) not null,
  apex_session varchar2(50) not null,
  apex_app     varchar2(10) not null
)
/

create or replace procedure do_login(
  p_apex_session in varchar2,
  p_apex_app     in varchar2,
  p_apex_page    in varchar2,
  p_apex_url     in varchar2,
  p_username     in varchar2,
  p_password     in varchar2
) is 
  l_sid          number;
  l_sid_created  boolean:=false;
begin
  if p_password = 'oracle' then 

    while not l_sid_created loop
      begin
        insert into my_sessions (sid, username, apex_session, apex_app) 
        values (floor(dbms_random.value(10000000000000000000,99999999999999999999)), p_username, p_apex_session, p_apex_app) returning sid into l_sid;
        l_sid_created := true;
      exception 
         when DUP_VAL_ON_INDEX then null;
         when others then raise;
      end;
    end loop;

    owa_cookie.send(
      NAME     => 'MY_REMOTE_USER',
      VALUE    => l_sid,
      EXPIRES  => null,
      PATH     => '/',
      DOMAIN   => '.de.oracle.com',
      SECURE   => null
    );
    owa_util.redirect_url(
      p_apex_url||'/f?p='||p_apex_app||':'||p_apex_page||':'||p_apex_session
    );
  else
    owa_util.redirect_url(
    sys_context('userenv','current_schema')||'.login_page'||
    '?p_apex_session='||p_apex_session||
    '&p_apex_app='||p_apex_app||
    '&p_apex_page='||p_apex_page||
    '&p_apex_url='||p_apex_url||
    '&p_msg=Fehlerhafter Login!'
   );
  end if;
end do_login;
/
sho err

grant execute on login_page to public
/
grant execute on do_login to public
/

Wiederum ist das Passwort stets oracle. Allerdings wird der Username nun nicht einfach ins Cookie, sondern mitsamt der APEX-Session-ID und der APEX-Anwendungs-ID in eine Tabelle (MY_SESSIONS) geschrieben. Eine "Session-ID" wird als 20-Stellige Zufallszahl ermittelt (damit man sie nicht erraten kann) und dann ins Cookie geschrieben. Eine Manipulation des Cookies bringt nun nichts mehr, weil der Eintrag in die Tabelle MY_SESSIONS fehlt. Allerdings muss APEX nun den Usernamen anhand des Session-ID ermitteln - wir brauchen also auch eine "umgekehrte" Funktion.

create or replace function get_username_by_ext_sessionid(p_sid in varchar2) return varchar2 is
  l_username varchar2(200);
begin
  begin
    select username into l_username 
    from my_sessions 
    where sid = to_number(p_sid) and apex_session = v('APP_SESSION') and apex_app = v('APP_ID');
  exception
    when NO_DATA_FOUND then l_username := null;
    when others then raise;
  end;
  return upper(l_username);
end;
/              

Das ist hier natürlich nur deshalb so einfach, weil sich der "externe Login-Server" doch in der gleichen Datenbank befindet wie die APEX-Anwendung. Wir müssen also nur in der Tabelle MY_SESSIONS nachsehen. Komplizierter wird es, wenn der "Login Server" tatsächlich extern ist - dann muss diese Funktion die Session-ID nochmals zum Login-Server senden, um den Usernamen herauszubekommen. Aber das ist auf jeden Fall besser als das Übertragen desselben per Browser.

Der Rest ist nun einfach: Navigieren Sie in Ihrer Anwendung nochmals zu den Gemeinsamen Komponenten , dort zu den Authentifizierungsschemas und dann zum neuen Authentifizierungsschema Cookie Auth. Tragen Sie dann bei den Einstellungen die PL/SQL Funktion zum Ableiten des Usernamens ein: get_username_by_ext_sessionid (Abbildung 14).

PL/SQL-Funktion zum Ableiten des Usernamens in den Plugin-Einstellungen hinterlegen

Abbildung 14: PL/SQL-Funktion zum Ableiten des Usernamens in den Plugin-Einstellungen hinterlegen

Nach dem Abspeichern können Sie die Anwendung direkt neu starten. Wiederum werden Sie auf das Login-Fenster verzweigt, wiederum müssen Sie sich mit einem beliebigen Usernamen und dem Passwort oracle anmelden, und wiederum sehen Sie danach Ihre APEX Seite wie in Abbildung 12. Wenn Sie nun aber ins Cookie schauen, sehen Sie den Unterschied (Abbildung 15).

Cookie Inhalt nach Absichern des Login-Verfahrens

Abbildung 15: Cookie Inhalt nach Absichern des Login-Verfahrens

Das Cookie enthält nun nur noch eine Zahl, die keinen Rückschluß auf den Usernamen zulässt. Man muss Zugriff auf die Tabelle MY_SESSIONS haben, um ein Cookie manipulieren zu können. Außerdem haben wir die Session-ID an die APEX-Applikation und an die APEX-Session gebunden. Wenn Sie nun die Session-ID aus der APEX-URL entfernen, kann das Cookie nicht mehr validiert werden und Sie landen wieder auf dem externen Login-Server. Das gleiche passiert, wenn Sie die Tabelle MY_SESSIONS leeren. Obwohl die APEX-Session prinzipiell intakt ist, scheitert die Validierung des Cookies und der Anwender muss sich neu einloggen. Ein Blick in die Tabelle MY_SESSIONS.

SQL> select * from my_sessions

                       SID USERNAME                 APEX_SESSION APEX_APP
-------------------------- ---------- -------------------------- --------
      36716362130478571103 joe                   965565288501301      137

Weitere Anmerkungen

Nun ist unser "Cookie-Basiertes" Login-Verfahren schon ein gutes Stück sicherer geworden. Allerdings gibt es noch ein paar Dinge zu beachten, die aber aus Platzgründen hier nicht mehr ausführlich dargestellt werden können.

  • Cookies werden zwischen Client und Server hin- und hergeschickt - auch wenn man aus der Session-ID den Nutzer nicht mehr direkt ableiten kann: Die Verschlüsselung über HTTPS ist sowohl für APEX als auch für den Login-Server ein Muss.
  • Der Inhalt des Cookies könnte zusätzlich nochmals verschlüsselt werden. Den dazu Schlüssel nötigen Schlüssel müssen nur die APEX-Anwendung und der Login-Server kennen. Gegebenenfalls kann man den Schlüssel auch regelmäßig ändern.
  • Die Tabelle MY_SESSIONS sollte mit Datenbankmitteln (wie der Virtual Private Database) gegen unerwünschte Manipulationen abgesichert werden.
  • Wie weiter oben bereits erwähnt, sehen Authentifizierungsplugins in APEX auch eine Post-Abmeldefunktion vor - die in diesem Beispiel nicht ausprogrammiert wurde. Es ist zu empfehlen, diese auszuprgrammieren und darin eine "Logout"-Funktion des Login-Server aufzurufen (pseudocode).
    function cookie_based_post_logout (
      p_authentication in wwv_flow_plugin_api.t_authentication,
      p_plugin         in wwv_flow_plugin_api.t_plugin 
    ) return wwv_flow_plugin_api.t_authentication_logout_result is
      l_result wwv_flow_plugin_api.t_authentication_logout_result;
    begin
      l_result.redirect_url := 
        'http://sccloud030.de.oracle.com:8080/apex/testit.logout_page?p_next_url='||
           wwv_flow_utilities.escape_url (
             p_url             => p_authentication.logout_url,
             p_escape_reserved => 'Y' 
           );
      return l_result;
    end cookie_based_post_logout;
    
    Die PL/SQL-Prozedur LOGOUT_PAGE des "Login Servers" löscht dann das Cookie MY_REMOTE_USER und invalidiert den Eintrag in der Tabelle MY_SESSIONS.

Fazit

APEX ist bereits seit dem ersten Tage in der Lage, sich in vorhandene Login-Verfahren zu integrieren, denn seit der ersten Version HTML DB 1.5 gibt es Authentifizierungsschemas. Mit APEX 4.1 wurde diese Fähigkeit lediglich neu organisiert und in die Plugin-Infrastruktur eingebunden, so dass sich Authentifizierungsschemas elegant als Plugin kapseln lassen. Auf Basis des hier beschriebenen Cookie-Basierten Verfahrens dürfte sich APEX mit jedem beliebigen Loginserver integrieren lassen - die hier angedachten Konzepte lassen sich durch Ändern des Plugin Code in jede Richtung erweitern und verändern.

Zurück zur Community-Seite