OAuth 2 og OIDC for Oracle REST

28. mai 2024

API, integrasjon, oracle, REST

ORDS logo

Tredje artikkel om Oracle REST Data Services omhandler hvordan et API beskyttes med OAuth 2 og OIDC.

OAuth 2 og OIDC for Oracle REST Data Services

Første del i denne serien introduserte Oracle REST Data Services (ORDS) og viste hvordan man lager et REST API på tabeller og views med noen få klikk i SQL Developer.

I Andre del ble ORDS API brukt til å definere et mer avansert REST API med litt mer kode.

I denne delen ser vi på sikkerhet, det vil si hvordan du beskytter et API med standard protokoller som OAuth 2 og OpenID Connect (OIDC). Detaljer rundt OAuth 2 og OIDC er det ikke plass til her, men for de som vil lære seg det inngående er det enkelt å se fra dokumentasjon hvordan ORDS støtter standarden. Dokumentasjon og enkel eksperimentering med ORDS er et godt utgangspunkt for å lære seg hvordan disse tingene henger sammen før man studerer dette i dybden.

Et API som tillater noe mer enn å hente ut åpne data må typisk beskyttes med autentisering og autorisering. Dette kan selvsagt gjøres på mange måter, men her vises hvordan det gjøres med funksjonalitet i ORDS.

Det er hovedsaklig to måter å gjøre det på med ORDS:

  1. Opprette klienter og roller lokalt med ORDS
  2. Bruke en ekstern leverandør som har ansvar for autentisering og utstedelse av token

Opprette klienter lokalt

Fordelen med dette er at det er enkelt og du har full kontroll selv. For en som er ny til OAuth er dette en god måte å komme i gang på, siden man har full kontroll og slipper å involvere andre i eventuell feilsøking. Ulempen er at du oppretter enda en ID-løsning i stedet for å overlate det til andre, dermed må klientene forholde seg til en ekstra bruker-konto i stedet for en sentralt styrt (tjeneste)bruker. En løsning med få brukere eller for å komme raskt i gang er dette et greit alternativt. (Og er den eksterne ID-leverandøren en sort boks langt borte er dette et fristende alternativ.)

I eksempel her brukes endepunkt /ords/admin/timereg/prosjekt/ opprettet i forrige artikkel . I følgende kodebit opprettes en rolle Prosjektadministrator og et privilegium timereg.prosjekt for endepunkt timereg/prosjekt/:

DECLARE
  l_priv_roles owa.vc_arr;
  l_priv_patterns owa.vc_arr;
  l_priv_modules owa.vc_arr;
BEGIN
  l_priv_roles(1) := 'Prosjektadministrator';
  l_priv_patterns(1) := '/timereg/prosjekt/'; -- Del av URL etter admin
  l_priv_modules(1) := 'timereg';
  ords.create_role(l_priv_roles(1));     
      
  ORDS.DEFINE_PRIVILEGE(
   p_privilege_name    => 'timereg.prosjekt',
   p_roles            => l_priv_roles,
   p_patterns         => l_priv_patterns,
   p_label            => 'Prosjektadministrator',
   p_description      => 'Privilegium for administrasjon av prosjekter',
   p_comments         => 'En berikende kommentar');
  commit;
end;
/

Privilegier og koblinger hentes ut slik:

select *
from user_ords_privileges ;
select *
from user_ords_privilege_mappings;

Referansedokumentasjon til ORDS-pakken er vist i kap 4 i *Developer’s Guide .

En ny test på samme url vil nå returnere 401 Unauthorized fordi det ikke finnes en bruker/klient med rollen. En lokal bruker i ORDS kan opprettes slik:

bin/ords --config ordsconfig config user add prosjekt_admin "Prosjektadministrator"

Brukernavn er altså prosjekt_admin og den får rollen Prosjektadministrator. ORDS må også restartes etter at bruker er lagt til. Eksempel med curl og basic authentication:

curl --user prosjekt_admin:PR2024  rio:8080/ords/admin/timereg/prosjekt/

Forsøker man i nettleser med url som vist får man samme feil med 401, men man kan logge inn ved å klikke på Sign in som vist her:

Feilmelding fra ORDS, Unauthorized

Lokal bruker som vist over er bare anvendelig til utvikling lokalt og i små systemer.

Pakken OAUTH brukes til å opprette OAuth-klienter. Her lages det en som får samme privilegium og rolle som opprettet over:

BEGIN
    oauth.create_client(
                       p_name => 'Timereg Administrator',
                       p_grant_type => 'client_credentials',
                       p_privilege_names => 'timereg.prosjekt',
                       p_support_email => 'support@example.com'
    );

    oauth.grant_client_role(
                           p_client_name => 'Timereg Administrator',
                           p_role_name => 'Prosjektadministrator'
    );
    COMMIT;
END;
/

Eksisterende klienter kan listes ut med:

select * 
from user_ords_clients;

For å få ut kun name, client_id og client_secret:

SELECT
    name,
    client_id,
    client_secret
FROM
    user_ords_clients;

Man trenger selvsagt ikke restarte ORDS etter at en client er opprettet slik (det gjelder kun lokale brukere som vist over.)

Et opaque access token kan hentes ut med:

 curl -i -k --user xZwkGJnZTZA2PkDlN_36FA..:D4PinTMYJEGugwD7Zqu_Dg.. \
 --data "grant_type=client_credentials"  rio:8080/ords/admin/oauth/token

Her er hhv CLIENT_ID og CLIENT_SECRET fra USER_ORDS_CLIENTS brukt i autentisering. ORDS returnerte her:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache, no-store, max-age=0
X-Frame-Options: SAMEORIGIN
Transfer-Encoding: chunked

{"access_token":"Xh9Bi8uI6tVBbtuHp_nQzQ","token_type":"bearer","expires_in":3600}

Token er gyldig i en time (3600 sekund), men det kan endres med parameter p_token_duration i oauth.create_client.

Med et token kan prosjekter nå hentes ut med:

curl -i -H "Authorization: Bearer Xh9Bi8uI6tVBbtuHp_nQzQ"   rio:8080/ords/admin/timereg/prosjekt/

Klienter opprettet med grant type client_credentials brukes i såkalt two-legged flow mellom to servere. For three-legged flow hvor en sluttbruker er involvert og skal godkjenne brukes authorization_code i parameter p_grant_type. I tillegg må p_redirect_uri settes til en url hvor authorization code sendes fra ORDS. Eksempel:

BEGIN
    oauth.create_client(
                       p_name => 'Timereg Frontend Administrator',
                       p_grant_type => 'authorization_code',
                       p_redirect_uri => 'https://enesi.no/timereg/auth/code/',
                       p_privilege_names => 'timereg.prosjekt',
                       p_support_email => 'support@example.com'
    );

    oauth.grant_client_role(
                           p_client_name => 'Timereg Frontend Administrator',
                           p_role_name => 'Prosjektadministrator'
    );
    COMMIT;
END;
/

Tim Hall aka Oracle-Base.com demonstrerer i en video på YouTube hvordan authorization flow settes opp i tilfelle du trenger litt variasjon.

ORDS støtter også Implicit flow med p_grant_type => 'implicit'. Referansedokumentasjon til OAUTH-pakken er vist i kap 6 i Developer’s Guide

Sletting av klient

Sletting av klient gjøres slik:

BEGIN
    oauth.delete_client('Webapp Timereg Administrator');
    COMMIT;
END;
/

Bruke ekstern IdP

IdP som i Identity Provider, feks din bedrifts sentrale løsning, Okta, Google, Azure, Oracle Cloud, etc.

Kort om JWT

JSON Web Token (JWT) utstedes av IdP og har en tredelt struktur:

JWT struktur

Et JWT (Wikipedia ) har en payload og en signatur som kan (dvs bør!) sjekkes slik at du er garantert at den kommer fra din IdP. Mange steder brukes det et access token i JWT-format og ikke et såkalt opaque token. Payload er naturligvis i JSON-format og kan lett behandles med JSON-funksjoner i SQL og PL/SQL som vist lenger ned.

JWT Profile

I ORDS kan du lage en JWT-profile pr skjema. Det vil si at ORDS ser etter Bearer token innsendt som JWT i Authorization header. ORDS støtter JWT som benyttes av kjente leverandører. Kravet for at ORDS skal gi tilgang til en ressurs er at JWT må inneholde et scope som er likt med privilege tilordnet ressursen (som vist tidligere med ORDS.DEFINE_PRIVILEGE).

Syntaks, hentet fra manualen:

OAUTH.CREATE_JWT_PROFILE (
       p_issuer       IN VARCHAR2,
       p_audience     IN VARCHAR2,
       p_jwk_url      IN VARCHAR2,
       p_description  IN VARCHAR2 DEFAULT NULL,
       p_allowed_skew IN NUMBER DEFAULT NULL,
       p_allowed_age  IN NUMBER DEFAULT NULL
   );

p_issuer og p_audience må stemme med hhv iss og aud claim i JWT som blir utstedt. p_jwk_uri er URI hvor JWT kan sjekkes. Dette kan finnes blant annet som i well known configuration til en OAuth provider. Eksempel fra Google:

curl https://accounts.google.com/.well-known/openid-configuration | jq '.jwks_uri'

Som gir: "https://www.googleapis.com/oauth2/v3/certs".

Manual lister opp parametre til kjente ID-leverandører som Okta, IDCS, OCI og Auth0.

Parse Bearer token selv

Om du har en egen OAuth-løsning kan det være at formatet ikke er helt som du ønsker. Det kan være at roller er pakket inn i eget claim. I så fall er det være aktuelt å plukke ut JWT selv som vist her:

l_authorization := regexp_substr(owa_util.get_cgi_env('Authorization'),'Bearer\s+(.+)',1,1,'i',1);
-- Bearer token must be JWT
l_token := apex_jwt.decode( p_value => l_authorization);
-- Validate, throws an exception when not valid
apex_jwt.validate(p_token => l_token,p_leeway_seconds => 10);
-- Fetch roles claim from payload
l_roles := json_value(l_token.payload,'$.roles');

Kode for å validere signatur er ikke vist her. Ditt API må naturligvis returnere 401 ved manglende token eller manglende rolle/scope i JWT.

Konklusjon

Sikkerhet er sjeldent trivielt, slik er det nesten uavhengig av teknologi, men med ORDS er det overkommelig. Det enkleste eksempelet på bruk av OpenID Connect jeg har sett er i Oracle APEX, se Social Sign-In .