OAuth 2.0 et JWT dans ABAP Cloud : Authentification basee sur les tokens

Catégorie
Security
Publié
Auteur
Johannes

OAuth 2.0 et JWT (JSON Web Tokens) sont les mecanismes d’authentification standard pour les applications cloud modernes. Dans ABAP Cloud, ils permettent une communication securisee de service a service et une authentification utilisateur sans mots de passe dans le code.

Concepts fondamentaux

Apercu OAuth 2.0

OAuth 2.0 est un framework d’autorisation qui permet aux applications un acces controle aux ressources :

TermeDescription
Resource OwnerL’utilisateur ou systeme proprietaire de la ressource
ClientL’application qui demande l’acces (par ex. ABAP Cloud)
Authorization ServerEmet les Access Tokens (par ex. XSUAA)
Resource ServerHeberge l’API protegee
Access TokenToken de courte duree pour l’acces API
Refresh TokenToken de longue duree pour le renouvellement des tokens

JWT (JSON Web Token)

JWT est le format de token utilise par OAuth 2.0 :

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyMTIzIiwiYXVkIjoibXktYXBwIiwiaXNzIjoieHN1YWEifQ.
signature_here

Un JWT se compose de trois parties (encodees en Base64, separees par des points) :

PartieContenu
HeaderAlgorithme et type de token
PayloadClaims (ID utilisateur, scopes, expiration)
SignatureSignature cryptographique pour verification

Claims JWT importants

ClaimDescription
subSubject - ID utilisateur ou client
audAudience - Public cible du token
issIssuer - Emetteur (par ex. URL XSUAA)
expExpiration - Timestamp Unix d’expiration
iatIssued At - Timestamp d’emission
scopeScopes d’autorisation
zidZone ID - Identifiant du tenant

Flows OAuth 2.0

Client Credentials Flow

Le Client Credentials Flow est utilise pour la communication service-a-service quand aucun utilisateur n’est implique :

+-------------------------------------------------------------------------+
| Client Credentials Flow |
| |
| +-------------+ +---------------------+ |
| | ABAP Cloud | | XSUAA | |
| | (Client) | | (Auth Server) | |
| +------+------+ +----------+----------+ |
| | | |
| | 1. POST /oauth/token | |
| | grant_type=client_credentials | |
| | client_id=xxx | |
| | client_secret=yyy | |
| |------------------------------------------------>| |
| | | |
| | 2. Access Token (JWT) | |
| |<------------------------------------------------| |
| | | |
| +------+------+ +----------+----------+ |
| | ABAP Cloud | | Resource Server | |
| | (Client) | | (API) | |
| +------+------+ +----------+----------+ |
| | | |
| | 3. Appel API avec Bearer Token | |
| | Authorization: Bearer <token> | |
| |------------------------------------------------>| |
| | | |
| | 4. Reponse API | |
| |<------------------------------------------------| |
+-------------------------------------------------------------------------+

Cas d’utilisation :

  • Communication backend-a-backend
  • Jobs batch sans contexte utilisateur
  • Scenarios d’integration technique

Authorization Code Flow

Le Authorization Code Flow est utilise quand un utilisateur est implique :

+-------------------------------------------------------------------------+
| Authorization Code Flow |
| |
| +---------+ +-------------+ +-------------+ +--------------+ |
| | Browser | | ABAP Cloud | | XSUAA | | Resource API | |
| +----+----+ +------+------+ +------+------+ +-------+------+ |
| | | | | |
| | 1. Login | | | |
| |--------------->| | | |
| | | | | |
| | 2. Redirect | | | |
| | vers XSUAA | | | |
| |<---------------| | | |
| | | | |
| | 3. Page de login | | |
| |---------------------------------->| | |
| | | | |
| | 4. Redirect avec Authorization Code | |
| |<----------------------------------| | |
| | | | | |
| | 5. Code | | | |
| |--------------->| | | |
| | | 6. Token Request | | |
| | |----------------->| | |
| | | 7. Access Token | | |
| | |<-----------------| | |
| | | | | |
| | | 8. Appel API | | |
| | |-------------------------------------->| |
| | | 9. Reponse | | |
| | |<--------------------------------------| |
+-------------------------------------------------------------------------+

Cas d’utilisation :

  • Applications web avec login utilisateur
  • Single Sign-On (SSO)
  • Applications Fiori

XSUAA dans SAP BTP

XSUAA (Extended Services for User Account and Authentication) est le serveur OAuth 2.0 central sur SAP BTP :

Service Binding XSUAA dans ABAP Cloud

Les credentials XSUAA sont fournis via un Service Binding :

CLASS zcl_xsuaa_config DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_xsuaa_credentials,
clientid TYPE string,
clientsecret TYPE string,
url TYPE string,
token_url TYPE string,
END OF ty_xsuaa_credentials.
CLASS-METHODS:
get_credentials
RETURNING VALUE(rs_credentials) TYPE ty_xsuaa_credentials
RAISING cx_abap_context_info_error.
ENDCLASS.
CLASS zcl_xsuaa_config IMPLEMENTATION.
METHOD get_credentials.
" Lire les credentials XSUAA depuis le Service Binding
" Ceux-ci sont automatiquement injectes par le systeme
DATA(lo_service_manager) = cl_ams_service_manager=>get_instance( ).
DATA(ls_binding) = lo_service_manager->get_service_binding(
iv_service_type = 'xsuaa"
).
" Extraire les credentials
rs_credentials-clientid = ls_binding-credentials-clientid.
rs_credentials-clientsecret = ls_binding-credentials-clientsecret.
rs_credentials-url = ls_binding-credentials-url.
rs_credentials-token_url = |{ ls_binding-credentials-url }/oauth/token|.
ENDMETHOD.
ENDCLASS.

Recuperer un token avec Client Credentials

Requete de token OAuth 2.0

CLASS zcl_oauth_client DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_token_response,
access_token TYPE string,
token_type TYPE string,
expires_in TYPE i,
scope TYPE string,
END OF ty_token_response.
METHODS:
constructor
IMPORTING is_credentials TYPE zcl_xsuaa_config=>ty_xsuaa_credentials,
get_access_token
RETURNING VALUE(rv_token) TYPE string
RAISING cx_web_http_client_error.
PRIVATE SECTION.
DATA ms_credentials TYPE zcl_xsuaa_config=>ty_xsuaa_credentials.
DATA ms_token_cache TYPE ty_token_response.
DATA mv_token_expiry TYPE timestamp.
METHODS:
request_new_token
RETURNING VALUE(rs_token) TYPE ty_token_response
RAISING cx_web_http_client_error,
is_token_valid
RETURNING VALUE(rv_valid) TYPE abap_bool.
ENDCLASS.
CLASS zcl_oauth_client IMPLEMENTATION.
METHOD constructor.
ms_credentials = is_credentials.
ENDMETHOD.
METHOD get_access_token.
" Utiliser le token du cache s'il est encore valide
IF is_token_valid( ) = abap_true.
rv_token = ms_token_cache-access_token.
RETURN.
ENDIF.
" Demander un nouveau token
ms_token_cache = request_new_token( ).
" Calculer l'expiration (avec 60 secondes de marge)
GET TIME STAMP FIELD DATA(lv_now).
mv_token_expiry = cl_abap_tstmp=>add(
tstmp = lv_now
secs = ms_token_cache-expires_in - 60
).
rv_token = ms_token_cache-access_token.
ENDMETHOD.
METHOD request_new_token.
" Creer le client HTTP pour l'endpoint token
DATA(lo_destination) = cl_http_destination_provider=>create_by_url(
i_url = ms_credentials-token_url
).
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination(
i_destination = lo_destination
).
DATA(lo_request) = lo_client->get_http_request( ).
" Authentification Basic pour Client Credentials
DATA(lv_auth_string) = |{ ms_credentials-clientid }:{ ms_credentials-clientsecret }|.
DATA(lv_auth_base64) = cl_web_http_utility=>encode_base64( lv_auth_string ).
lo_request->set_header_field(
i_name = 'Authorization"
i_value = |Basic { lv_auth_base64 }|
).
lo_request->set_header_field(
i_name = 'Content-Type"
i_value = 'application/x-www-form-urlencoded"
).
" Corps de la requete token
lo_request->set_text( 'grant_type=client_credentials' ).
" Executer la requete
DATA(lo_response) = lo_client->execute( if_web_http_client=>post ).
DATA(lv_status) = lo_response->get_status( )-code.
IF lv_status <> 200.
lo_client->close( ).
RAISE EXCEPTION TYPE cx_web_http_client_error.
ENDIF.
" Parser la reponse
DATA(lv_json) = lo_response->get_text( ).
lo_client->close( ).
/ui2/cl_json=>deserialize(
EXPORTING json = lv_json
CHANGING data = rs_token
).
ENDMETHOD.
METHOD is_token_valid.
IF ms_token_cache-access_token IS INITIAL.
rv_valid = abap_false.
RETURN.
ENDIF.
GET TIME STAMP FIELD DATA(lv_now).
rv_valid = xsdbool( lv_now < mv_token_expiry ).
ENDMETHOD.
ENDCLASS.

Token avec Communication Arrangement

Dans les scenarios productifs, les Communication Arrangements devraient etre utilises :

CLASS zcl_oauth_comm_arrangement DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
call_protected_api
IMPORTING iv_path TYPE string
RETURNING VALUE(rv_response) TYPE string
RAISING cx_web_http_client_error
cx_http_dest_provider_error.
ENDCLASS.
CLASS zcl_oauth_comm_arrangement IMPLEMENTATION.
METHOD call_protected_api.
" Destination avec OAuth 2.0 Client Credentials depuis Communication Arrangement
" La destination gere automatiquement les tokens
DATA(lo_destination) = cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = 'Z_EXTERNAL_API"
service_id = 'Z_EXT_API_REST"
comm_system_id = 'EXT_SYSTEM"
).
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination(
i_destination = lo_destination
).
DATA(lo_request) = lo_client->get_http_request( ).
lo_request->set_uri_path( iv_path ).
" Le token est automatiquement ajoute par la destination
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
rv_response = lo_response->get_text( ).
lo_client->close( ).
ENDMETHOD.
ENDCLASS.

Validation des tokens JWT

Parser la structure du token

CLASS zcl_jwt_parser DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_jwt_header,
alg TYPE string,
typ TYPE string,
kid TYPE string,
END OF ty_jwt_header,
BEGIN OF ty_jwt_payload,
sub TYPE string,
aud TYPE string,
iss TYPE string,
exp TYPE i,
iat TYPE i,
scope TYPE string,
zid TYPE string,
client_id TYPE string,
END OF ty_jwt_payload,
BEGIN OF ty_jwt_parts,
header TYPE ty_jwt_header,
payload TYPE ty_jwt_payload,
signature TYPE string,
END OF ty_jwt_parts.
METHODS:
parse
IMPORTING iv_token TYPE string
RETURNING VALUE(rs_parts) TYPE ty_jwt_parts
RAISING cx_abap_invalid_value.
PRIVATE SECTION.
METHODS:
decode_base64url
IMPORTING iv_encoded TYPE string
RETURNING VALUE(rv_decoded) TYPE string.
ENDCLASS.
CLASS zcl_jwt_parser IMPLEMENTATION.
METHOD parse.
" Diviser le token en trois parties
SPLIT iv_token AT '.' INTO DATA(lv_header_b64)
DATA(lv_payload_b64)
DATA(lv_signature).
IF lv_header_b64 IS INITIAL OR lv_payload_b64 IS INITIAL.
RAISE EXCEPTION TYPE cx_abap_invalid_value.
ENDIF.
" Decoder et parser le header
DATA(lv_header_json) = decode_base64url( lv_header_b64 ).
/ui2/cl_json=>deserialize(
EXPORTING json = lv_header_json
CHANGING data = rs_parts-header
).
" Decoder et parser le payload
DATA(lv_payload_json) = decode_base64url( lv_payload_b64 ).
/ui2/cl_json=>deserialize(
EXPORTING json = lv_payload_json
CHANGING data = rs_parts-payload
).
rs_parts-signature = lv_signature.
ENDMETHOD.
METHOD decode_base64url.
" Convertir Base64URL en Base64 standard
DATA(lv_base64) = iv_encoded.
REPLACE ALL OCCURRENCES OF '-' IN lv_base64 WITH '+'.
REPLACE ALL OCCURRENCES OF '_' IN lv_base64 WITH '/'.
" Ajouter le padding si necessaire
DATA(lv_padding) = strlen( lv_base64 ) MOD 4.
IF lv_padding > 0.
lv_base64 = lv_base64 && repeat( val = '=' occ = 4 - lv_padding ).
ENDIF.
" Decoder Base64
rv_decoded = cl_web_http_utility=>decode_base64( lv_base64 ).
ENDMETHOD.
ENDCLASS.

Implementer la validation de token

CLASS zcl_jwt_validator DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_validation_result,
valid TYPE abap_bool,
error_msg TYPE string,
jwt_payload TYPE zcl_jwt_parser=>ty_jwt_payload,
END OF ty_validation_result.
METHODS:
constructor
IMPORTING iv_expected_issuer TYPE string
iv_expected_audience TYPE string,
validate
IMPORTING iv_token TYPE string
RETURNING VALUE(rs_result) TYPE ty_validation_result.
PRIVATE SECTION.
DATA mv_expected_issuer TYPE string.
DATA mv_expected_audience TYPE string.
METHODS:
validate_expiration
IMPORTING iv_exp TYPE i
RETURNING VALUE(rv_valid) TYPE abap_bool,
validate_issuer
IMPORTING iv_issuer TYPE string
RETURNING VALUE(rv_valid) TYPE abap_bool,
validate_audience
IMPORTING iv_audience TYPE string
RETURNING VALUE(rv_valid) TYPE abap_bool.
ENDCLASS.
CLASS zcl_jwt_validator IMPLEMENTATION.
METHOD constructor.
mv_expected_issuer = iv_expected_issuer.
mv_expected_audience = iv_expected_audience.
ENDMETHOD.
METHOD validate.
DATA(lo_parser) = NEW zcl_jwt_parser( ).
TRY.
DATA(ls_jwt) = lo_parser->parse( iv_token ).
CATCH cx_abap_invalid_value.
rs_result-valid = abap_false.
rs_result-error_msg = 'Format de token invalide'.
RETURN.
ENDTRY.
" 1. Verifier l'expiration
IF validate_expiration( ls_jwt-payload-exp ) = abap_false.
rs_result-valid = abap_false.
rs_result-error_msg = 'Token expire'.
RETURN.
ENDIF.
" 2. Verifier l'emetteur
IF validate_issuer( ls_jwt-payload-iss ) = abap_false.
rs_result-valid = abap_false.
rs_result-error_msg = |Emetteur invalide : { ls_jwt-payload-iss }|.
RETURN.
ENDIF.
" 3. Verifier l'audience
IF validate_audience( ls_jwt-payload-aud ) = abap_false.
rs_result-valid = abap_false.
rs_result-error_msg = |Audience invalide : { ls_jwt-payload-aud }|.
RETURN.
ENDIF.
" Token valide
rs_result-valid = abap_true.
rs_result-jwt_payload = ls_jwt-payload.
ENDMETHOD.
METHOD validate_expiration.
" Convertir Unix Timestamp en ABAP Timestamp
DATA(lv_exp_ts) = cl_abap_tstmp=>utclong_unix_from_i( iv_exp ).
GET TIME STAMP FIELD DATA(lv_now).
rv_valid = xsdbool( lv_now < lv_exp_ts ).
ENDMETHOD.
METHOD validate_issuer.
rv_valid = xsdbool( iv_issuer CS mv_expected_issuer ).
ENDMETHOD.
METHOD validate_audience.
rv_valid = xsdbool( iv_audience = mv_expected_audience ).
ENDMETHOD.
ENDCLASS.

Authentifier les requetes entrantes

Quand votre application ABAP Cloud agit comme Resource Server :

Extraire le JWT de la requete HTTP

CLASS zcl_request_authenticator DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
authenticate_request
IMPORTING io_request TYPE REF TO if_web_http_request
RETURNING VALUE(rs_result) TYPE zcl_jwt_validator=>ty_validation_result.
PRIVATE SECTION.
METHODS:
extract_bearer_token
IMPORTING io_request TYPE REF TO if_web_http_request
RETURNING VALUE(rv_token) TYPE string.
ENDCLASS.
CLASS zcl_request_authenticator IMPLEMENTATION.
METHOD authenticate_request.
" Extraire le Bearer Token du header Authorization
DATA(lv_token) = extract_bearer_token( io_request ).
IF lv_token IS INITIAL.
rs_result-valid = abap_false.
rs_result-error_msg = 'Aucun Bearer token fourni'.
RETURN.
ENDIF.
" Valider le token
DATA(lo_validator) = NEW zcl_jwt_validator(
iv_expected_issuer = 'https://mysubaccount.authentication.eu10.hana.ondemand.com"
iv_expected_audience = 'my-application-clientid"
).
rs_result = lo_validator->validate( lv_token ).
ENDMETHOD.
METHOD extract_bearer_token.
DATA(lv_auth_header) = io_request->get_header_field( 'Authorization' ).
IF lv_auth_header CP 'Bearer *'.
rv_token = lv_auth_header+7. " 'Bearer ' = 7 caracteres
ENDIF.
ENDMETHOD.
ENDCLASS.

Autorisation basee sur les scopes

CLASS zcl_scope_authorizer DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
has_scope
IMPORTING iv_scope_string TYPE string
iv_required_scope TYPE string
RETURNING VALUE(rv_has_scope) TYPE abap_bool,
check_scope
IMPORTING iv_scope_string TYPE string
iv_required_scope TYPE string
RAISING zcx_not_authorized.
ENDCLASS.
CLASS zcl_scope_authorizer IMPLEMENTATION.
METHOD has_scope.
" Les scopes sont separes par des espaces
SPLIT iv_scope_string AT ' ' INTO TABLE DATA(lt_scopes).
READ TABLE lt_scopes TRANSPORTING NO FIELDS
WITH KEY table_line = iv_required_scope.
rv_has_scope = xsdbool( sy-subrc = 0 ).
ENDMETHOD.
METHOD check_scope.
IF has_scope(
iv_scope_string = iv_scope_string
iv_required_scope = iv_required_scope
) = abap_false.
RAISE EXCEPTION TYPE zcx_not_authorized
EXPORTING
textid = zcx_not_authorized=>missing_scope
scope = iv_required_scope.
ENDIF.
ENDMETHOD.
ENDCLASS.

Bonnes pratiques

A faire

RecommandationJustification
Utiliser Communication ArrangementsGestion automatique des tokens
Mettre en cache les tokensMoins de requetes vers le serveur d’auth
Courte duree de vie des tokensMinimise les degats en cas de fuite
Scopes minimauxPrincipe du moindre privilege
Validation des tokens cote serveurJamais uniquement cote client

A eviter

A eviterRisque
Secrets dans le codeFaille de securite
Tokens dans les logsExposition des credentials
Tokens dans les URLsVisible dans l’historique du navigateur
Ne pas verifier la signatureFalsification de token possible
Accepter les tokens expiresAttaques par rejeu

Sujets complementaires

Resume

OAuth 2.0 et JWT sont essentiels pour les applications cloud securisees :

  1. Client Credentials Flow pour la communication service-a-service sans utilisateur
  2. Authorization Code Flow pour l’authentification basee sur l’utilisateur
  3. XSUAA est le serveur OAuth 2.0 central sur SAP BTP
  4. Validation JWT comprend la verification de signature, expiration, emetteur et audience
  5. Communication Arrangements simplifient considerablement la gestion des tokens
  6. Scopes permettent une autorisation fine

Utilisez Communication Arrangements et Destinations pour les scenarios productifs - ils gerent automatiquement le cycle de vie complet des tokens.