CREATE OR REPLACE PACKAGE digest_auth_api AS
-- --------------------------------------------------------------------------
-- Name         : https://oracle-base.com/dba/miscellaneous/digest_auth_api.sql
-- Author       : DBA Learning Hub
-- Description  : API to allow digest authentication when using UTL_HTTP.
--                The aim is this only replaces UTL_HTTP.BEGIN_REQUEST.
--                All other coding (wallet handling and processing the response)
--                are still done by you, in the normal way.
--
-- References   : This is heavily inspired by the blog post by Gary Myers.
--                http://blog.sydoracle.com/2014/03/plsql-utlhttp-and-digest-authentication.html
--                I make liberal use of the ideas, and in some cases the code, he discussed in
--                that blog post!
--                For setting up certificates and wallets, see this article.
--                https://oracle-base.com/articles/misc/utl_http-and-ssl
--
-- License      : Free for personal and commercial use.
--                You can amend the code, but leave existing the headers, current
--                amendments history and links intact.
--                Copyright and disclaimer available here:
--                https://oracle-base.com/misc/site-info.php#copyright
-- Ammedments   :
--   When         Who       What
--   ===========  ========  =================================================
--   11-DEC-2015  Tim Hall  Initial Creation
--   30-JUN-2016  Tim Hall  Add debug_on and debug_off procedures.
-- --------------------------------------------------------------------------

/*
Example call.

SET SERVEROUTPUT ON
DECLARE
  l_url            VARCHAR2(32767) := 'https://example.com/ws/get-something';
  l_http_request   UTL_HTTP.req;
  l_http_response  UTL_HTTP.resp;
  l_text           VARCHAR2(32767);
BEGIN
  -- Set wallet credentials.
  UTL_HTTP.set_wallet('file:/path/to/wallet', 'wallet-password');

  -- Make a HTTP request and get the response.
  l_http_request  := digest_auth_api.begin_request(p_url          => l_url,
                                                   p_username     => 'my-username',
                                                   p_password     => 'my-password',
                                                   p_method       => 'GET');

  l_http_response := UTL_HTTP.get_response(l_http_request);

  -- Loop through the response.
  BEGIN
    LOOP
      UTL_HTTP.read_text(l_http_response, l_text, 32767);
      DBMS_OUTPUT.put_line (l_text);
    END LOOP;
  EXCEPTION
    WHEN UTL_HTTP.end_of_body THEN
      UTL_HTTP.end_response(l_http_response);
  END;

EXCEPTION
  WHEN OTHERS THEN
    UTL_HTTP.end_response(l_http_response);
    RAISE;
END;

*/
-- --------------------------------------------------------------------------

PROCEDURE debug_on;

PROCEDURE debug_off;

FUNCTION begin_request(p_url          IN VARCHAR2,
                       p_username     IN VARCHAR2,
                       p_password     IN VARCHAR2,
                       p_method       IN VARCHAR2 DEFAULT 'GET',
                       p_http_version IN VARCHAR2 DEFAULT 'HTTP/1.1',
                       p_req_cnt      IN PLS_INTEGER DEFAULT 1)
  RETURN UTL_HTTP.req;

END digest_auth_api;
/
SHOW ERRORS


CREATE OR REPLACE PACKAGE BODY digest_auth_api AS
-- --------------------------------------------------------------------------
-- Name         : https://oracle-base.com/dba/miscellaneous/digest_auth_api.sql
-- Author       : DBA Learning Hub
-- Description  : API to allow digest authentication when using UTL_HTTP.
--                The aim is this only replaces UTL_HTTP.BEGIN_REQUEST.
--                All other coding (wallet handling and processing the response)
--                are still done by you, in the normal way.
--
-- References   : This is heavily inspired by the blog post by Gary Myers.
--                http://blog.sydoracle.com/2014/03/plsql-utlhttp-and-digest-authentication.html
--                I make liberal use of the ideas, and in some cases the code, he discussed in
--                that blog post!
--                For setting up certificates and wallets, see this article.
--                https://oracle-base.com/articles/misc/utl_http-and-ssl
--
-- License      : Free for personal and commercial use.
--                You can amend the code, but leave existing the headers, current
--                amendments history and links intact.
--                Copyright and disclaimer available here:
--                https://oracle-base.com/misc/site-info.php#copyright
-- Ammedments   :
--   When         Who       What
--   ===========  ========  =================================================
--   11-DEC-2015  Tim Hall  Initial Creation
--   30-JUN-2016  Tim Hall  Add debug_on and debug_off procedures.
-- --------------------------------------------------------------------------

-- Package variables.
g_debug   BOOLEAN := FALSE;

-- Set by call to get_header_info.
g_server  VARCHAR2(32767);
g_realm   VARCHAR2(32767);
g_qop     VARCHAR2(32767);
g_nonce   VARCHAR2(32767);
g_opaque  VARCHAR2(32767);
g_cnonce  VARCHAR2(32767);

-- Prototypes.
PROCEDURE debug (p_text IN VARCHAR2);

PROCEDURE init;

PROCEDURE get_header_info (p_http_response IN OUT NOCOPY UTL_HTTP.resp);

FUNCTION get_response (p_username IN VARCHAR2,
                       p_password IN VARCHAR2,
                       p_uri      IN VARCHAR2,
                       p_method   IN VARCHAR2 DEFAULT 'GET',
                       p_req_cnt  IN NUMBER DEFAULT 1)
RETURN VARCHAR2;



-- Real stuff starts here.

-- -----------------------------------------------------------------------------
PROCEDURE debug_on AS
BEGIN
  g_debug := TRUE;
END debug_on;
-- -----------------------------------------------------------------------------



-- -----------------------------------------------------------------------------
PROCEDURE debug_off AS
BEGIN
  g_debug := FALSE;
END debug_off;
-- -----------------------------------------------------------------------------



-- -----------------------------------------------------------------------------
PROCEDURE debug (p_text IN VARCHAR2) AS
BEGIN
  IF g_debug THEN
    DBMS_OUTPUT.put_line(p_text);
  END IF;
END debug;
-- -----------------------------------------------------------------------------



-- -----------------------------------------------------------------------------
PROCEDURE init IS
BEGIN
  g_server  := NULL;
  g_realm   := NULL;
  g_qop     := NULL;
  g_nonce   := NULL;
  g_opaque  := NULL;
  g_cnonce  := NULL;
END init;
-- -----------------------------------------------------------------------------



-- -----------------------------------------------------------------------------
PROCEDURE get_header_info (p_http_response IN OUT NOCOPY UTL_HTTP.resp) IS

  l_name            VARCHAR2(256);
  l_value           VARCHAR2(1024);
BEGIN
  FOR i IN 1..UTL_HTTP.get_header_count(p_http_response) LOOP
    UTL_HTTP.get_header(p_http_response, i, l_name, l_value);
    debug('------ Header (' || i || ') ------');
    debug('l_name=' || l_name);
    debug('l_value=' || l_value);
    IF l_name = 'Server' THEN
      g_server := l_value;
      debug('g_server=' || g_server);
    END IF;

    IF l_name = 'WWW-Authenticate' THEN
      g_realm  := SUBSTR(REGEXP_SUBSTR(l_value, 'realm="[^"]+' ),8);
      g_qop    := SUBSTR(REGEXP_SUBSTR(l_value, 'qop="[^"]+'   ),6);
      g_nonce  := SUBSTR(REGEXP_SUBSTR(l_value, 'nonce="[^"]+' ),8);
      g_opaque := SUBSTR(REGEXP_SUBSTR(l_value, 'opaque="[^"]+'),9);

      debug('g_realm=' || g_realm);
      debug('g_qop=' || g_qop);
      debug('g_nonce=' || g_nonce);
      debug('g_opaque=' || g_opaque);
    END IF;
  END LOOP;

  g_cnonce := LOWER(UTL_RAW.cast_to_raw(DBMS_OBFUSCATION_TOOLKIT.md5(input_string => DBMS_RANDOM.value)));
  debug('g_cnonce=' || g_cnonce);
END get_header_info;
-- -----------------------------------------------------------------------------



-- -----------------------------------------------------------------------------
FUNCTION get_response (p_username IN VARCHAR2,
                       p_password IN VARCHAR2,
                       p_uri      IN VARCHAR2,
                       p_method   IN VARCHAR2 DEFAULT 'GET',
                       p_req_cnt  IN NUMBER DEFAULT 1)
RETURN VARCHAR2 IS
  l_text      VARCHAR2(2000);
  l_raw       RAW(2000);
  l_out       VARCHAR2(60);
  l_ha1       VARCHAR2(40);
  l_ha2       VARCHAR2(40);
BEGIN
  l_text := p_username || ':' || g_realm || ':' || p_password;
  l_raw  := UTL_RAW.cast_to_raw(l_text);
  l_out  := DBMS_OBFUSCATION_TOOLKIT.md5(input => l_raw);
  l_ha1  := LOWER(l_out);

  l_text := p_method || ':' || p_uri;
  l_raw  := UTL_RAW.cast_to_raw(l_text);
  l_out  := DBMS_OBFUSCATION_TOOLKIT.md5(input => l_raw);
  l_ha2  := LOWER(l_out);

  l_text := l_ha1 || ':' || g_nonce || ':' || LPAD(p_req_cnt,8,0) || ':' || g_cnonce || ':' || g_qop || ':' || l_ha2;
  l_raw  := UTL_RAW.cast_to_raw(l_text);
  l_out  := DBMS_OBFUSCATION_TOOLKIT.md5(input => l_raw);

  RETURN LOWER(l_out);
END get_response;
-- -----------------------------------------------------------------------------



-- -----------------------------------------------------------------------------
FUNCTION begin_request(p_url          IN VARCHAR2,
                       p_username     IN VARCHAR2,
                       p_password     IN VARCHAR2,
                       p_method       IN VARCHAR2 DEFAULT 'GET',
                       p_http_version IN VARCHAR2 DEFAULT 'HTTP/1.1',
                       p_req_cnt      IN PLS_INTEGER DEFAULT 1)
  RETURN UTL_HTTP.req
AS
  l_http_request   UTL_HTTP.req;
  l_http_response  UTL_HTTP.resp;
  l_text           VARCHAR2(32767);
  l_uri            VARCHAR2(32767);
  l_response       VARCHAR2(32767);
BEGIN
  init;

  -- Make a request that will fail to get the header information.
  -- This will be used to build up the pieces for the digest authentication
  -- using a call to get_header_info.
  l_http_request  := UTL_HTTP.begin_request(p_url, p_method);
  l_http_response := UTL_HTTP.get_response(l_http_request);
  get_header_info (l_http_response);
  UTL_HTTP.end_response(l_http_response);

  -- Get everything after the domain as the URI.
  l_uri := SUBSTR(p_url, INSTR(p_url, '/', 1, 3));

  l_response := get_response(p_username => p_username,
                             p_password => p_password,
                             p_uri      => l_uri,
                             p_method   => p_method,
                             p_req_cnt  => p_req_cnt);

  -- Build the final digest string.
  l_text := 'Digest username="' || p_username ||'",'||
            ' realm="'          || g_realm ||'",'||
            ' nonce="'          || g_nonce ||'",'||
            ' uri="'            || l_uri ||'",'||
            ' response="'       || l_response ||'",'||
            ' qop='             || g_qop ||',' ||
            ' nc='              || LPAD(p_req_cnt,8,0) ||',' ||
            ' cnonce="'         || g_cnonce      ||'"';

  IF g_opaque IS NOT NULL THEN
    l_text := l_text || ',opaque="'||g_opaque||'"';
  END IF;
  debug(l_text);

  -- Make the new request and set the digest authorization.
  l_http_request  := UTL_HTTP.begin_request(p_url, p_method);
  UTL_HTTP.SET_HEADER(l_http_request, 'Authorization', l_text);

  RETURN l_http_request;
EXCEPTION
  WHEN OTHERS THEN
    UTL_HTTP.end_response(l_http_response);
    RAISE;
END begin_request;
-- -----------------------------------------------------------------------------

END digest_auth_api;
/
SHOW ERRORS
