From 36b817b405b891899ac4d61d68d14b409f7521bb Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Fri, 13 Jan 2017 20:21:55 +0100 Subject: [PATCH 01/14] Support GitHub two factor authentication --- src/zabapgit.prog.abap | 1 + src/zabapgit_2fa.prog.abap | 586 +++++++++++++++++++++++++ src/zabapgit_2fa.prog.xml | 48 ++ src/zabapgit_http.prog.abap | 98 ++++- src/zabapgit_password_dialog.prog.abap | 110 ++++- src/zabapgit_persistence.prog.abap | 90 ++++ 6 files changed, 908 insertions(+), 25 deletions(-) create mode 100644 src/zabapgit_2fa.prog.abap create mode 100644 src/zabapgit_2fa.prog.xml diff --git a/src/zabapgit.prog.abap b/src/zabapgit.prog.abap index 1c7a15d73..b81885ed0 100644 --- a/src/zabapgit.prog.abap +++ b/src/zabapgit.prog.abap @@ -53,6 +53,7 @@ INCLUDE zabapgit_stage. INCLUDE zabapgit_git_helpers. INCLUDE zabapgit_repo. INCLUDE zabapgit_stage_logic. +INCLUDE zabapgit_2fa. INCLUDE zabapgit_http. INCLUDE zabapgit_git. INCLUDE zabapgit_objects. diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap new file mode 100644 index 000000000..c41e0acb5 --- /dev/null +++ b/src/zabapgit_2fa.prog.abap @@ -0,0 +1,586 @@ +*&---------------------------------------------------------------------* +*& Include ZABAPGIT_2FA +*&---------------------------------------------------------------------* + +"! Exception base class for two factor authentication related errors +CLASS lcx_2fa_error DEFINITION INHERITING FROM cx_static_check. + PUBLIC SECTION. + METHODS: + constructor IMPORTING is_textid LIKE textid OPTIONAL + ix_previous LIKE previous OPTIONAL + iv_error_text TYPE csequence OPTIONAL, + get_text REDEFINITION. + DATA: + mv_text TYPE string READ-ONLY. + PROTECTED SECTION. + METHODS: + get_default_text RETURNING VALUE(rv_text) TYPE string. +ENDCLASS. + +CLASS lcx_2fa_error IMPLEMENTATION. + METHOD constructor. + super->constructor( textid = is_textid previous = ix_previous ). + mv_text = iv_error_text. + ENDMETHOD. + + METHOD get_text. + IF mv_text IS NOT INITIAL. + result = mv_text. + ELSEIF get_default_text( ) IS NOT INITIAL. + result = get_default_text( ). + ELSE. + result = super->get_text( ). + ENDIF. + ENDMETHOD. + + METHOD get_default_text. + rv_text = 'Error in two factor authentication.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_auth_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_auth_failed IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Authentication failed using 2FA.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_token_gen_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_token_gen_failed IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Two factor access token generation failed.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_unsupported DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_unsupported IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'The service is not supported for two factor authentication.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_no_cached_token DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_no_cached_token IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Cached two factor access token requested but not available.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_cache_deletion_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_cache_deletion_failed IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Cache deletion failed.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +"! Defines a two factor authentication authenticator +"!

+"! Authenticators support one or multiple services and are able to generate access tokens using the +"! service's API using the users username, password and two factor authentication token +"! (app/sms/tokengenerator). With these access tokens the user can be authenticated to the service's +"! implementation of the git http api, just like the "normal" password would. The authenticator can +"! also store and retrieve the access token it generated. +"!

+"!

+"! LCL_2FA_AUTHENTICATOR_REGISTRY can be used to find a suitable implementation for a given +"! repository. +"!

+INTERFACE lif_2fa_authenticator. + METHODS: + "! Generate an access token + "! @parameter iv_url | Repository url + "! @parameter iv_username | Username + "! @parameter iv_password | Password + "! @parameter iv_2fa_token | Two factor token + "! @parameter rv_access_token | Generated access token + "! @raising lcx_2fa_auth_failed | Authentication failed + "! @raising lcx_2fa_token_gen_failed | Token generation failed + authenticate IMPORTING iv_url TYPE string + iv_username TYPE string + iv_password TYPE string + iv_2fa_token TYPE string + RETURNING VALUE(rv_access_token) TYPE string + RAISING lcx_2fa_auth_failed + lcx_2fa_token_gen_failed, + "! Check if this authenticator instance supports the give repository url + "! @parameter iv_url | Repository url + "! @parameter rv_supported | Is supported + supports_url IMPORTING iv_url TYPE string + RETURNING VALUE(rv_supported) TYPE abap_bool, + "! Get a unique identifier for the service that hosts the repository + "! @parameter iv_url | Repository url + "! @parameter rv_id | Service id + get_service_id_from_url IMPORTING iv_url TYPE string + RETURNING VALUE(rv_id) TYPE string, + "! Check if there is a cached access token (for the current user) + "! @parameter iv_url | Repository url + "! @parameter rv_available | Token is cached + is_cached_access_token_avail IMPORTING iv_url TYPE string + RETURNING VALUE(rv_available) TYPE abap_bool, + "! Get a cached access token + "!

+ "! Username and password are also parameters to decrypt the token if needed. They must no + "! necessarily be provided if the used authenticator does not use encryption. + "!

+ "! @parameter iv_url | Repository url + "! @parameter iv_username | Username + "! @parameter iv_password | Password + "! @parameter rv_token | Access token + "! @raising lcx_2fa_no_cached_token | There is no cached token + get_cached_access_token IMPORTING iv_url TYPE string + iv_username TYPE string OPTIONAL + iv_password TYPE string OPTIONAL + RETURNING VALUE(rv_token) TYPE string + RAISING lcx_2fa_no_cached_token, + "! Delete a cached token + "! @parameter iv_url | Repository url + "! @raising lcx_2fa_cache_deletion_failed | Deletion failed + delete_cached_access_token IMPORTING iv_url TYPE string + RAISING lcx_2fa_cache_deletion_failed. +ENDINTERFACE. + +"! Default LIF_2FA-AUTHENTICATOR implememtation +"!

+"! This uses the user settings to store cached access tokens and encrypts / decrypts them as needed. +"!

+CLASS lcl_2fa_authenticator_base DEFINITION + ABSTRACT + CREATE PUBLIC. + + PUBLIC SECTION. + INTERFACES: + lif_2fa_authenticator. + ALIASES: + authenticate FOR lif_2fa_authenticator~authenticate, + supports_url FOR lif_2fa_authenticator~supports_url, + get_service_id_from_url FOR lif_2fa_authenticator~get_service_id_from_url, + is_cached_access_token_avail FOR lif_2fa_authenticator~is_cached_access_token_avail, + get_cached_access_token FOR lif_2fa_authenticator~get_cached_access_token, + delete_cached_token FOR lif_2fa_authenticator~delete_cached_access_token. + METHODS: + "! @parameter iv_supported_url_regex | Regular expression to check if a repository url is + "! supported, used for default implementation of + "! SUPPORTS_URL + constructor IMPORTING iv_supported_url_regex TYPE clike. + PROTECTED SECTION. + METHODS: + "! Subclass implementation of LIF_2FA_AUTHENTICATOR=>AUTHENTICATE + "!

+ "! The caller will take care of caching the token. + "!

+ "! @parameter iv_url | Repository url + "! @parameter iv_username | Username + "! @parameter iv_password | Password + "! @parameter iv_2fa_token | Two factor token + "! @parameter rv_access_token | Generated access token + "! @raising lcx_2fa_auth_failed | Authentication failed + "! @raising lcx_2fa_token_gen_failed | Token generation failed + authenticate_internal ABSTRACT IMPORTING iv_url TYPE string + iv_username TYPE string + iv_password TYPE string + iv_2fa_token TYPE string + RETURNING VALUE(rv_access_token) TYPE string + RAISING lcx_2fa_auth_failed + lcx_2fa_token_gen_failed, + "! Helper method to raise class based exception after traditional exception was raised + "!

+ "! sy-msg... must be set right before calling! + "!

+ raise_internal_error_from_sy FINAL RAISING lcx_2fa_auth_failed. + PRIVATE SECTION. + DATA: + mo_url_regex TYPE REF TO cl_abap_regex. +ENDCLASS. + +CLASS lcl_2fa_authenticator_base IMPLEMENTATION. + METHOD constructor. + CREATE OBJECT mo_url_regex + EXPORTING + pattern = iv_supported_url_regex + ignore_case = abap_true. + ENDMETHOD. + + METHOD authenticate. + DATA: lv_encrypted_token TYPE string. + + rv_access_token = authenticate_internal( iv_url = iv_url + iv_username = iv_username + iv_password = iv_password + iv_2fa_token = iv_2fa_token ). + + " Store the access token, by default in the user settings + + " 1. Encrypt it +* lv_encrypted_token = cl_encryption_helper=>encrypt_symmetric( iv_text = rv_access_token +* iv_key = iv_password ). + " TODO: Find something like the above for symmetric encryption + lv_encrypted_token = rv_access_token. + + " 2. Store it + TRY. + lcl_app=>user( )->set_2fa_access_token( iv_service_id = get_service_id_from_url( iv_url ) + iv_username = iv_username + iv_token = lv_encrypted_token ). + CATCH lcx_exception ##NO_HANDLER. + " Not the biggest of deals if caching the token fails + ENDTRY. + ENDMETHOD. + + METHOD supports_url. + rv_supported = mo_url_regex->create_matcher( text = iv_url )->match( ). + ENDMETHOD. + + METHOD get_service_id_from_url. + rv_id = 'UNKNOWN SERVICE'. " Please overwrite in subclasses + ENDMETHOD. + + METHOD is_cached_access_token_avail. + DATA: lv_service_id TYPE string. + + lv_service_id = get_service_id_from_url( iv_url ). + + " Default storage location is user settings + TRY. + rv_available = boolc( lcl_app=>user( )->get_2fa_access_token( lv_service_id ) + IS NOT INITIAL ). + CATCH lcx_exception. + rv_available = abap_false. + ENDTRY. + ENDMETHOD. + + METHOD get_cached_access_token. + DATA: lv_access_token_encrypted TYPE string, + lx_error TYPE REF TO cx_root. + + TRY. + lv_access_token_encrypted + = lcl_app=>user( )->get_2fa_access_token( get_service_id_from_url( iv_url ) ). + CATCH lcx_exception INTO lx_error. + RAISE EXCEPTION TYPE lcx_2fa_no_cached_token + EXPORTING + ix_previous = lx_error + iv_error_text = lx_error->get_text( ). + ENDTRY. + + IF lv_access_token_encrypted IS INITIAL. + RAISE EXCEPTION TYPE lcx_2fa_no_cached_token. + ENDIF. + + " TODO: Decryption +* rv_token = cl_encryption_helper=>decrypt_symmetric( iv_encrypted = rv_access_token +* iv_key = iv_password ). + rv_token = lv_access_token_encrypted. + ENDMETHOD. + + METHOD delete_cached_token. + DATA: lx_ex TYPE REF TO cx_root. + + TRY. + " Default storage location is user settings + lcl_app=>user( )->delete_2fa_config( get_service_id_from_url( iv_url ) ). + CATCH lcx_exception INTO lx_ex. + RAISE EXCEPTION TYPE lcx_2fa_cache_deletion_failed + EXPORTING + ix_previous = lx_ex + iv_error_text = |Cache deletion failed: { lx_ex->get_text( ) }|. + ENDTRY. + ENDMETHOD. + + METHOD raise_internal_error_from_sy. + DATA: lv_error_msg TYPE string. + + MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno + WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 + INTO lv_error_msg. + RAISE EXCEPTION TYPE lcx_2fa_auth_failed + EXPORTING + iv_error_text = |Internal error: { lv_error_msg }| ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcl_2fa_github_authenticator DEFINITION + INHERITING FROM lcl_2fa_authenticator_base + FINAL + CREATE PUBLIC. + + PUBLIC SECTION. + METHODS: + constructor, + get_service_id_from_url REDEFINITION. + PROTECTED SECTION. + METHODS: + authenticate_internal REDEFINITION. + PRIVATE SECTION. + METHODS: + set_access_token_request IMPORTING ii_entity TYPE REF TO if_rest_entity + iv_repo_name TYPE string, + get_token_from_response IMPORTING ii_entity TYPE REF TO if_rest_entity + RETURNING VALUE(rv_token) TYPE string, + parse_repo_from_url IMPORTING iv_url TYPE string + RETURNING VALUE(rv_repo_name) TYPE string. +ENDCLASS. + +CLASS lcl_2fa_github_authenticator IMPLEMENTATION. + METHOD constructor. + super->constructor( 'https?:\/\/(www\.)?github.com.*$' ). + ENDMETHOD. + + METHOD authenticate_internal. + CONSTANTS: lc_github_api_url TYPE string VALUE `https://api.github.com/`, + lc_otp_header_name TYPE string VALUE `X-Github-OTP`, + lc_restendpoint_authorizations TYPE string VALUE `/authorizations`. + DATA: li_rest_client TYPE REF TO if_rest_client, + li_http_client TYPE REF TO if_http_client, + lv_http_code TYPE i, + lv_http_code_description TYPE string, + li_request_entity TYPE REF TO if_rest_entity, + li_response_entity TYPE REF TO if_rest_entity, + lv_binary_response TYPE xstring, + BEGIN OF ls_success_response, + token TYPE string, + END OF ls_success_response. + + " 1. Try to login to GitHub API with username, password and 2fa token + cl_http_client=>create_by_url( + EXPORTING + url = lc_github_api_url + IMPORTING + client = li_http_client + EXCEPTIONS + argument_not_found = 1 + plugin_not_active = 2 + internal_error = 3 + OTHERS = 4 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + " https://developer.github.com/v3/auth/#working-with-two-factor-authentication + li_http_client->propertytype_accept_cookie = if_http_client=>co_enabled. + li_http_client->request->set_header_field( name = lc_otp_header_name value = iv_2fa_token ). + li_http_client->authenticate( username = iv_username password = iv_password ). + li_http_client->propertytype_logon_popup = if_http_client=>co_disabled. + + li_http_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + li_http_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + " Check if authentication has succeeded + li_http_client->response->get_status( + IMPORTING + code = lv_http_code + reason = lv_http_code_description + ). + IF lv_http_code <> 200. + RAISE EXCEPTION TYPE lcx_2fa_auth_failed + EXPORTING + iv_error_text = |Authentication failed: { lv_http_code_description }|. + ENDIF. + + " Authentication worked, now the rest client can be used + CREATE OBJECT li_rest_client TYPE cl_rest_http_client + EXPORTING + io_http_client = li_http_client. + + + " 2. Create an access token which can be used instead of a password + " https://developer.github.com/v3/oauth_authorizations/#create-a-new-authorization + + li_request_entity = li_rest_client->create_request_entity( ). + set_access_token_request( ii_entity = li_request_entity + iv_repo_name = parse_repo_from_url( iv_url ) ). + li_rest_client->set_request_header( iv_name = if_http_header_fields_sap=>request_uri + iv_value = lc_restendpoint_authorizations ). + li_rest_client->post( li_request_entity ). + + lv_http_code = li_rest_client->get_status( ). + IF lv_http_code <> 201. + RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed + EXPORTING + iv_error_text = |Token generation failed: { lv_http_code }|. + ENDIF. + + rv_access_token = get_token_from_response( li_rest_client->get_response_entity( ) ). + IF rv_access_token IS INITIAL. + RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed. + ENDIF. + ENDMETHOD. + + METHOD set_access_token_request. + CONSTANTS: BEGIN OF lc_create_access_token_request, + scopes TYPE string VALUE 'repo', + note TYPE string VALUE 'abapGit', + END OF lc_create_access_token_request. + DATA: lo_json_writer TYPE REF TO cl_sxml_string_writer, + lt_scopes TYPE stringtab, + lt_rest_parvalues TYPE abap_trans_srcbind_tab, + ls_rest_line LIKE LINE OF lt_rest_parvalues, + lt_result_parvalues TYPE abap_trans_resbind_tab, + ls_result_line LIKE LINE OF lt_result_parvalues, + lr_data_ref TYPE REF TO data, + lv_note TYPE string, + lv_fingerprint TYPE string. + + lo_json_writer = cl_sxml_string_writer=>create( type = if_sxml=>co_xt_json ). + APPEND lc_create_access_token_request-scopes TO lt_scopes. + + GET REFERENCE OF lc_create_access_token_request-scopes INTO lr_data_ref. + ls_rest_line-name = 'scopes'. + ls_rest_line-value = lr_data_ref. + APPEND ls_rest_line TO lt_rest_parvalues. + + GET REFERENCE OF lc_create_access_token_request-note INTO lr_data_ref. + ls_rest_line-name = 'note'. + ls_rest_line-value = lr_data_ref. + APPEND ls_rest_line TO lt_rest_parvalues. + + " The fingerprint must be unique, otherwise only one token can be generated, unless the user + " deletes it in Github's settings. This is problematic if he deletes it in abapGit but keeps it + " on GitHub. + lv_fingerprint = |abapGit-{ sy-sysid }-{ sy-uname }-{ sy-datum }-{ sy-uzeit }|. + GET REFERENCE OF lv_fingerprint INTO lr_data_ref. + ls_rest_line-name = 'fingerprint'. + ls_rest_line-value = lr_data_ref. + APPEND ls_rest_line TO lt_rest_parvalues. + + " Dynamic source table is used because otherwise identifiers will always be written in uppercase + " which is not supported by the GitHub's API. + CALL TRANSFORMATION id SOURCE (lt_rest_parvalues) + RESULT XML lo_json_writer. + + ii_entity->set_string_data( cl_abap_codepage=>convert_from( lo_json_writer->get_output( ) ) ). + ENDMETHOD. + + METHOD get_token_from_response. + CONSTANTS: lc_token_field_name TYPE string VALUE 'token'. + DATA: lt_result_parvalues TYPE abap_trans_resbind_tab, + ls_result_line LIKE LINE OF lt_result_parvalues, + lr_data_ref TYPE REF TO data, + lv_binary_response TYPE xstring. + + GET REFERENCE OF rv_token INTO lr_data_ref. + ls_result_line-name = lc_token_field_name. + ls_result_line-value = lr_data_ref. + APPEND ls_result_line TO lt_result_parvalues. + + lv_binary_response = ii_entity->get_binary_data( ). + CALL TRANSFORMATION id SOURCE XML lv_binary_response + RESULT (lt_result_parvalues). + ENDMETHOD. + + METHOD parse_repo_from_url. + DATA: lo_regex TYPE REF TO cl_abap_regex, + lo_matcher TYPE REF TO cl_abap_matcher. + + CREATE OBJECT lo_regex + EXPORTING + pattern = 'https?:\/\/(www\.)?github.com\/(.*)$'. + + lo_matcher = lo_regex->create_matcher( text = iv_url ). + IF lo_matcher->match( ) = abap_true. + rv_repo_name = lo_matcher->get_submatch( 1 ). + ELSE. + rv_repo_name = '???' ##NO_TEXT. + ENDIF. + ENDMETHOD. + + METHOD get_service_id_from_url. + rv_id = 'github'. + ENDMETHOD. +ENDCLASS. + +"! Static registry class to find LIF_2FA_AUTHENTICATOR instances +CLASS lcl_2fa_authenticator_registry DEFINITION + FINAL + CREATE PRIVATE. + + PUBLIC SECTION. + CLASS-METHODS: + class_constructor, + "! Retrieve an authenticator instance by url + "! @parameter iv_url | Url of the repository / service + "! @parameter ro_authenticator | Found authenticator instance + "! @raising lcx_2fa_unsupported | No authenticator found that supports the service + get_authenticator_for_url IMPORTING iv_url TYPE string + RETURNING VALUE(ro_authenticator) TYPE REF TO lif_2fa_authenticator + RAISING lcx_2fa_unsupported, + "! Check if there is a two factor authenticator available for the url + "! @parameter iv_url | Url of the repository / service + "! @parameter rv_supported | 2FA is supported + is_url_supported IMPORTING iv_url TYPE string + RETURNING VALUE(rv_supported) TYPE abap_bool. + CLASS-DATA: + "! All authenticators managed by the registry + gt_registered_authenticators TYPE HASHED TABLE OF REF TO lif_2fa_authenticator + WITH UNIQUE KEY table_line READ-ONLY. + PROTECTED SECTION. + PRIVATE SECTION. +ENDCLASS. + +CLASS lcl_2fa_authenticator_registry IMPLEMENTATION. + METHOD class_constructor. + DEFINE register. + CREATE OBJECT li_authenticator TYPE &1. + INSERT li_authenticator INTO TABLE gt_registered_authenticators. + END-OF-DEFINITION. + + DATA: li_authenticator TYPE REF TO lif_2fa_authenticator. + + " If there are new authenticators these need to be added here manually. + " I do not think there is an equivalent to SEO_INTERFACE_IMPLEM_GET_ALL for local classes + " without invoking the compiler directly. + register: lcl_2fa_github_authenticator. + ENDMETHOD. + + METHOD get_authenticator_for_url. + FIELD-SYMBOLS: LIKE LINE OF gt_registered_authenticators. + + LOOP AT gt_registered_authenticators ASSIGNING . + IF ->supports_url( iv_url ) = abap_true. + ro_authenticator = . + RETURN. + ENDIF. + ENDLOOP. + + RAISE EXCEPTION TYPE lcx_2fa_unsupported. + ENDMETHOD. + + METHOD is_url_supported. + TRY. + get_authenticator_for_url( iv_url ). + rv_supported = abap_true. + CATCH lcx_2fa_unsupported ##NO_HANDLER. + ENDTRY. + ENDMETHOD. +ENDCLASS. \ No newline at end of file diff --git a/src/zabapgit_2fa.prog.xml b/src/zabapgit_2fa.prog.xml new file mode 100644 index 000000000..84863cedb --- /dev/null +++ b/src/zabapgit_2fa.prog.xml @@ -0,0 +1,48 @@ + + + + + + ZABAPGIT_2FA + A + + + X + S + D$ + + + + I + + + + 0000-00-00 + + 0000-00-00 + + + + + + X + + 0000-00-00 + + 0000-00-00 + + D$S + X + + + + R + + ZABAPGIT_2FA + 12 + + + + + + diff --git a/src/zabapgit_http.prog.abap b/src/zabapgit_http.prog.abap index 11c1fb551..bca230808 100644 --- a/src/zabapgit_http.prog.abap +++ b/src/zabapgit_http.prog.abap @@ -469,21 +469,82 @@ CLASS lcl_http IMPLEMENTATION. METHOD acquire_login_details. - DATA: lv_default_user TYPE string, - lv_user TYPE string, - lv_pass TYPE string, - lo_digest TYPE REF TO lcl_http_digest. + DATA: lv_default_user TYPE string, + lv_user TYPE string, + lv_pass TYPE string, + lv_2fa_token TYPE string, + lv_access_token TYPE string, + lo_digest TYPE REF TO lcl_http_digest, + lv_2fa_available TYPE abap_bool, + li_authenticator TYPE REF TO lif_2fa_authenticator, + lx_error TYPE REF TO cx_root, + lv_popup_mode TYPE lcl_password_dialog=>gty_mode, + lv_popup_requested_token_del TYPE abap_bool, + lv_service_id TYPE string. lv_default_user = lcl_app=>user( )->get_repo_username( iv_url ). lv_user = lv_default_user. + + IF lcl_2fa_authenticator_registry=>is_url_supported( iv_url ) = abap_true. + TRY. + li_authenticator = lcl_2fa_authenticator_registry=>get_authenticator_for_url( iv_url ). + lv_service_id = li_authenticator->get_service_id_from_url( iv_url ). + IF li_authenticator->is_cached_access_token_avail( iv_url ) = abap_true. + lv_popup_mode = lcl_password_dialog=>gc_modes-unlock_2fa_token. + ELSE. + lv_popup_mode = lcl_password_dialog=>gc_modes-user_pass_2fa. + ENDIF. + + CATCH lcx_2fa_error. + lv_2fa_available = abap_false. + CLEAR lv_access_token. + ENDTRY. + ELSE. + lv_popup_mode = lcl_password_dialog=>gc_modes-user_pass. + ENDIF. + lcl_password_dialog=>popup( EXPORTING - iv_repo_url = iv_url + iv_repo_url = iv_url + iv_mode = lv_popup_mode + IMPORTING + ev_delete_token = lv_popup_requested_token_del CHANGING - cv_user = lv_user - cv_pass = lv_pass ). + cv_user = lv_user + cv_pass = lv_pass + cv_2fa_token = lv_2fa_token ). + + IF lv_popup_requested_token_del = abap_true. + TRY. + li_authenticator->delete_cached_access_token( lv_service_id ). + CATCH lcx_2fa_cache_deletion_failed INTO lx_error. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = lx_error->get_text( ) + ix_previous = lx_error. + ENDTRY. + + " Cancel authentication, no credentials were provided. This will cause the next http request + " somewhere up the callstack to result in a 401 error. + RETURN. + ENDIF. + + " Unlock cached access token + IF lv_popup_mode = lcl_password_dialog=>gc_modes-unlock_2fa_token. + TRY. + ASSERT li_authenticator IS BOUND. + lv_access_token = li_authenticator->get_cached_access_token( iv_url = iv_url + iv_username = lv_user + iv_password = lv_pass ). + CATCH lcx_2fa_no_cached_token INTO lx_error. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = lx_error->get_text( ) + ix_previous = lx_error. + ENDTRY. + ENDIF. IF lv_user IS INITIAL. lcx_exception=>raise( 'HTTP 401, unauthorized' ). @@ -494,6 +555,29 @@ CLASS lcl_http IMPLEMENTATION. iv_username = lv_user ). ENDIF. + IF lv_access_token IS INITIAL AND lv_2fa_token IS NOT INITIAL. + " There is no cached access token but the user provided a two factor token to generate a new + " access token + TRY. + ASSERT li_authenticator IS BOUND. + lv_access_token = li_authenticator->authenticate( iv_url = iv_url + iv_username = lv_user + iv_password = lv_pass + iv_2fa_token = lv_2fa_token ). + CATCH lcx_2fa_error INTO lx_error. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = lx_error->get_text( ) + ix_previous = lx_error. + ENDTRY. + ENDIF. + + " If there is an access token by now use that as the password instead because two factor + " authentication was requested. + IF lv_access_token IS NOT INITIAL. + lv_pass = lv_access_token. + ENDIF. + rv_scheme = ii_client->response->get_header_field( 'www-authenticate' ). FIND REGEX '^(\w+)' IN rv_scheme SUBMATCHES rv_scheme. diff --git a/src/zabapgit_password_dialog.prog.abap b/src/zabapgit_password_dialog.prog.abap index 354f0d44d..b87406890 100644 --- a/src/zabapgit_password_dialog.prog.abap +++ b/src/zabapgit_password_dialog.prog.abap @@ -16,6 +16,13 @@ SELECTION-SCREEN BEGIN OF LINE. SELECTION-SCREEN COMMENT 1(10) s_pass FOR FIELD p_pass. PARAMETERS: p_pass TYPE string LOWER CASE VISIBLE LENGTH 40 ##SEL_WRONG. SELECTION-SCREEN END OF LINE. +SELECTION-SCREEN BEGIN OF LINE. +PARAMETERS: p_en2fa TYPE abap_bool DEFAULT abap_false USER-COMMAND u1 MODIF ID m1 AS CHECKBOX. +SELECTION-SCREEN COMMENT 4(6) s_2fat FOR FIELD p_2fat MODIF ID m1. +SELECTION-SCREEN POSITION 12. +PARAMETERS: p_2fat TYPE string LOWER CASE VISIBLE LENGTH 40 MODIF ID m1. +SELECTION-SCREEN END OF LINE. +SELECTION-SCREEN FUNCTION KEY 1. SELECTION-SCREEN END OF SCREEN 1002. *----------------------------------------------------------------------- @@ -24,14 +31,26 @@ SELECTION-SCREEN END OF SCREEN 1002. CLASS lcl_password_dialog DEFINITION FINAL. PUBLIC SECTION. - CONSTANTS dynnr TYPE char4 VALUE '1002'. + TYPES: + gty_mode TYPE i. + CONSTANTS: + dynnr TYPE char4 VALUE '1002', + BEGIN OF gc_modes, + user_pass TYPE gty_mode VALUE 1, + user_pass_2fa TYPE gty_mode VALUE 2, + unlock_2fa_token TYPE gty_mode VALUE 3, + END OF gc_modes. CLASS-METHODS popup IMPORTING - iv_repo_url TYPE string + iv_repo_url TYPE string + iv_mode TYPE gty_mode DEFAULT gc_modes-user_pass + EXPORTING + ev_delete_token TYPE abap_bool CHANGING - cv_user TYPE string - cv_pass TYPE string. + cv_user TYPE string + cv_pass TYPE string + cv_2fa_token TYPE string. CLASS-METHODS on_screen_init. CLASS-METHODS on_screen_output. @@ -40,7 +59,10 @@ CLASS lcl_password_dialog DEFINITION FINAL. iv_ucomm TYPE syucomm. PRIVATE SECTION. - CLASS-DATA mv_confirm TYPE abap_bool. + CLASS-DATA: + mv_confirm TYPE abap_bool, + gv_mode TYPE gty_mode, + gv_delete_token TYPE abap_bool. ENDCLASS. "lcl_password_dialog DEFINITION @@ -52,17 +74,22 @@ CLASS lcl_password_dialog IMPLEMENTATION. p_url = iv_repo_url. p_user = cv_user. mv_confirm = abap_false. + gv_mode = iv_mode. + gv_delete_token = abap_false. CALL SELECTION-SCREEN dynnr STARTING AT 5 5 ENDING AT 60 8. IF mv_confirm = abap_true. cv_user = p_user. cv_pass = p_pass. + cv_2fa_token = p_2fat. ELSE. - CLEAR: cv_user, cv_pass. + CLEAR: cv_user, cv_pass, cv_2fa_token. ENDIF. - CLEAR: p_url, p_user, p_pass. + ev_delete_token = gv_delete_token. + + CLEAR: p_url, p_user, p_pass, p_2fat. ENDMETHOD. "popup @@ -71,6 +98,7 @@ CLASS lcl_password_dialog IMPLEMENTATION. s_url = 'Repo URL' ##NO_TEXT. s_user = 'User' ##NO_TEXT. s_pass = 'Password' ##NO_TEXT. + s_2fat = '2FA' ##NO_TEXT. ENDMETHOD. "on_screen_init METHOD on_screen_output. @@ -78,19 +106,51 @@ CLASS lcl_password_dialog IMPLEMENTATION. ASSERT sy-dynnr = dynnr. + CLEAR p_2fat. + LOOP AT SCREEN. - IF screen-name = 'P_URL'. - screen-input = '0'. - screen-intensified = '1'. - screen-display_3d = '0'. - MODIFY SCREEN. - ENDIF. - IF screen-name = 'P_PASS'. - screen-invisible = '1'. + CASE screen-name. + WHEN 'P_URL'. + screen-input = '0'. + screen-intensified = '1'. + screen-display_3d = '0'. + MODIFY SCREEN. + + WHEN 'P_PASS'. + screen-invisible = '1'. + MODIFY SCREEN. + + WHEN 'P_USER'. + IF gv_mode = gc_modes-unlock_2fa_token. + screen-input = 0. + MODIFY SCREEN. + ENDIF. + + WHEN 'P_2FAT'. + IF p_en2fa = abap_true. + screen-input = '1'. + ELSE. + screen-input = '0'. + p_2fat = 'Two factor authentication token' ##NO_TEXT. + ENDIF. + MODIFY SCREEN. + + ENDCASE. + + IF screen-group1 = 'M1' AND gv_mode <> gc_modes-user_pass_2fa. + screen-invisible = '1'. MODIFY SCREEN. ENDIF. ENDLOOP. + IF gv_mode = gc_modes-unlock_2fa_token. + s_title = 'Unlock two factor authentication token' ##NO_TEXT. + sscrfields-functxt_01 = 'Remove token' ##NO_TEXT. + ELSE. + s_title = 'Login'. + CLEAR sscrfields-functxt_01. + ENDIF. + " Program RSSYSTDB, GUI Status %_CSP PERFORM set_pf_status IN PROGRAM rsdbrunt IF FOUND. APPEND 'NONE' TO lt_ucomm. "Button Check @@ -102,28 +162,42 @@ CLASS lcl_password_dialog IMPLEMENTATION. TABLES p_exclude = lt_ucomm. - IF p_user IS NOT INITIAL. + IF p_user IS NOT INITIAL AND p_pass IS NOT INITIAL AND p_en2fa = abap_true. + SET CURSOR FIELD 'P_2FAT'. + ELSEIF p_user IS NOT INITIAL. SET CURSOR FIELD 'P_PASS'. ENDIF. ENDMETHOD. "on_screen_output METHOD on_screen_event. + DATA: lv_answer TYPE c. ASSERT sy-dynnr = dynnr. " CRET - F8 " OTHERS - simulate Enter press CASE iv_ucomm. + WHEN 'FC01'. " Delete two factor code + CALL FUNCTION 'POPUP_TO_CONFIRM' + EXPORTING + text_question = 'Do you really want to remove this two factor access token?' + IMPORTING + answer = lv_answer. + IF lv_answer = '1'. + gv_delete_token = abap_true. + LEAVE TO SCREEN 0. + ENDIF. WHEN 'CRET'. mv_confirm = abap_true. + LEAVE TO SCREEN 0. WHEN OTHERS. "TODO REFACTOR !!! A CLUTCH ! " This will work unless any new specific logic appear " for other commands. The problem is that the password dialog " does not have Enter event (or I don't know how to activate it ;) " so Enter issues previous command from previous screen " But for now this works :) Fortunately Esc produces another flow - mv_confirm = abap_true. - LEAVE TO SCREEN 0. +* mv_confirm = abap_true. +* LEAVE TO SCREEN 0. ENDCASE. ENDMETHOD. "on_screen_event diff --git a/src/zabapgit_persistence.prog.abap b/src/zabapgit_persistence.prog.abap index 7232279af..e8e8d96a1 100644 --- a/src/zabapgit_persistence.prog.abap +++ b/src/zabapgit_persistence.prog.abap @@ -406,6 +406,21 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. RETURNING VALUE(rv_email) TYPE string RAISING lcx_exception. + METHODS get_2fa_access_token + IMPORTING iv_service_id TYPE string + RETURNING VALUE(rv_token) TYPE string + RAISING lcx_exception. + + METHODS set_2fa_access_token + IMPORTING iv_service_id TYPE string + iv_username TYPE string + iv_token TYPE string + RAISING lcx_exception. + + METHODS delete_2fa_config + IMPORTING iv_service_id TYPE string + RAISING lcx_exception. + METHODS toggle_hide_files RETURNING VALUE(rv_hide) TYPE abap_bool RAISING lcx_exception. @@ -455,6 +470,13 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. END OF ty_repo_config. TYPES: ty_repo_config_tt TYPE STANDARD TABLE OF ty_repo_config WITH DEFAULT KEY. + TYPES: BEGIN OF ty_git_hosting_service, + service_id TYPE string, + username TYPE string, + access_token TYPE string, + END OF ty_git_hosting_service. + TYPES: ty_git_hosting_service_tab TYPE STANDARD TABLE OF ty_git_hosting_service WITH DEFAULT KEY. + TYPES: BEGIN OF ty_user, username TYPE string, email TYPE string, @@ -464,6 +486,7 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. changes_only TYPE abap_bool, diff_unified TYPE abap_bool, favorites TYPE tt_favorites, + two_fact_cfg TYPE ty_git_hosting_service_tab, END OF ty_user. METHODS constructor @@ -496,6 +519,16 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. is_repo_config TYPE ty_repo_config RAISING lcx_exception. + METHODS read_2fa_config + IMPORTING iv_service_id TYPE ty_git_hosting_service-service_id + RETURNING VALUE(rs_2fa_config) TYPE ty_git_hosting_service + RAISING lcx_exception. + + METHODS update_2fa_config + IMPORTING iv_service_id TYPE ty_git_hosting_service-service_id + is_2fa_config TYPE ty_git_hosting_service + RAISING lcx_exception. + ENDCLASS. "lcl_persistence_user DEFINITION CLASS lcl_persistence_user IMPLEMENTATION. @@ -639,6 +672,48 @@ CLASS lcl_persistence_user IMPLEMENTATION. ENDMETHOD. "update_repo_config + METHOD read_2fa_config. + DATA: lt_2fa_config TYPE ty_git_hosting_service_tab, + lv_key TYPE string. + + lv_key = to_lower( iv_service_id ). + lt_2fa_config = read( )-two_fact_cfg. + READ TABLE lt_2fa_config INTO rs_2fa_config WITH KEY service_id = lv_key. + ENDMETHOD. + + METHOD update_2fa_config. + DATA: ls_user TYPE ty_user, + lv_key TYPE string. + FIELD-SYMBOLS TYPE ty_git_hosting_service. + + ls_user = read( ). + lv_key = to_lower( iv_service_id ). + + READ TABLE ls_user-two_fact_cfg ASSIGNING WITH KEY service_id = lv_key. + IF sy-subrc IS NOT INITIAL. + APPEND INITIAL LINE TO ls_user-two_fact_cfg ASSIGNING . + ENDIF. + = is_2fa_config. + -service_id = lv_key. + + update( ls_user ). + ENDMETHOD. + + METHOD delete_2fa_config. + DATA: ls_user TYPE ty_user, + lv_key TYPE string. + + ls_user = read( ). + lv_key = to_lower( iv_service_id ). + + DELETE ls_user-two_fact_cfg WHERE service_id = lv_key. + IF sy-subrc <> 0. + lcx_exception=>raise( '2FA config could not be deleted.' ) ##NO_TEXT. + ENDIF. + + update( ls_user ). + ENDMETHOD. + METHOD set_repo_username. DATA: ls_repo_config TYPE ty_repo_config. @@ -671,6 +746,21 @@ CLASS lcl_persistence_user IMPLEMENTATION. ENDMETHOD. "get_repo_email + METHOD get_2fa_access_token. + rv_token = read_2fa_config( iv_service_id )-access_token. + ENDMETHOD. + + METHOD set_2fa_access_token. + DATA: ls_config TYPE ty_git_hosting_service. + + ls_config = read_2fa_config( iv_service_id ). + ls_config-service_id = iv_service_id. + ls_config-username = iv_username. + ls_config-access_token = iv_token. + + update_2fa_config( iv_service_id = iv_service_id is_2fa_config = ls_config ). + ENDMETHOD. + METHOD toggle_hide_files. DATA ls_user TYPE ty_user. From cb0161d3e332d64b79dc0ef1a166b3488ba1d227 Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sat, 14 Jan 2017 18:22:53 +0100 Subject: [PATCH 02/14] Remove usages of REST API for compatibility --- src/zabapgit_2fa.prog.abap | 48 +++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index c41e0acb5..57487717c 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -337,9 +337,9 @@ CLASS lcl_2fa_github_authenticator DEFINITION authenticate_internal REDEFINITION. PRIVATE SECTION. METHODS: - set_access_token_request IMPORTING ii_entity TYPE REF TO if_rest_entity + set_access_token_request IMPORTING ii_request TYPE REF TO if_http_request iv_repo_name TYPE string, - get_token_from_response IMPORTING ii_entity TYPE REF TO if_rest_entity + get_token_from_response IMPORTING ii_response TYPE REF TO if_http_response RETURNING VALUE(rv_token) TYPE string, parse_repo_from_url IMPORTING iv_url TYPE string RETURNING VALUE(rv_repo_name) TYPE string. @@ -354,12 +354,9 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. CONSTANTS: lc_github_api_url TYPE string VALUE `https://api.github.com/`, lc_otp_header_name TYPE string VALUE `X-Github-OTP`, lc_restendpoint_authorizations TYPE string VALUE `/authorizations`. - DATA: li_rest_client TYPE REF TO if_rest_client, - li_http_client TYPE REF TO if_http_client, + DATA: li_http_client TYPE REF TO if_http_client, lv_http_code TYPE i, lv_http_code_description TYPE string, - li_request_entity TYPE REF TO if_rest_entity, - li_response_entity TYPE REF TO if_rest_entity, lv_binary_response TYPE xstring, BEGIN OF ls_success_response, token TYPE string, @@ -408,30 +405,38 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. iv_error_text = |Authentication failed: { lv_http_code_description }|. ENDIF. - " Authentication worked, now the rest client can be used - CREATE OBJECT li_rest_client TYPE cl_rest_http_client - EXPORTING - io_http_client = li_http_client. - " 2. Create an access token which can be used instead of a password " https://developer.github.com/v3/oauth_authorizations/#create-a-new-authorization - li_request_entity = li_rest_client->create_request_entity( ). - set_access_token_request( ii_entity = li_request_entity + set_access_token_request( ii_request = li_http_client->request iv_repo_name = parse_repo_from_url( iv_url ) ). - li_rest_client->set_request_header( iv_name = if_http_header_fields_sap=>request_uri - iv_value = lc_restendpoint_authorizations ). - li_rest_client->post( li_request_entity ). + li_http_client->request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = lc_restendpoint_authorizations ). + li_http_client->request->set_method( if_http_request=>co_request_method_post ). - lv_http_code = li_rest_client->get_status( ). + li_http_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + li_http_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + li_http_client->response->get_status( + IMPORTING + code = lv_http_code + reason = lv_http_code_description + ). IF lv_http_code <> 201. RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed EXPORTING - iv_error_text = |Token generation failed: { lv_http_code }|. + iv_error_text = |Token generation failed: { lv_http_code } { lv_http_code_description }|. ENDIF. - rv_access_token = get_token_from_response( li_rest_client->get_response_entity( ) ). + rv_access_token = get_token_from_response( li_http_client->response ). IF rv_access_token IS INITIAL. RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed. ENDIF. @@ -479,7 +484,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. CALL TRANSFORMATION id SOURCE (lt_rest_parvalues) RESULT XML lo_json_writer. - ii_entity->set_string_data( cl_abap_codepage=>convert_from( lo_json_writer->get_output( ) ) ). + ii_request->set_data( lo_json_writer->get_output( ) ). ENDMETHOD. METHOD get_token_from_response. @@ -494,7 +499,8 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ls_result_line-value = lr_data_ref. APPEND ls_result_line TO lt_result_parvalues. - lv_binary_response = ii_entity->get_binary_data( ). + lv_binary_response = ii_response->get_data( ). + CALL TRANSFORMATION id SOURCE XML lv_binary_response RESULT (lt_result_parvalues). ENDMETHOD. From dd516fbea1ea2a84bb4c2e3c4c6f99f048756cf1 Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sat, 14 Jan 2017 18:35:52 +0100 Subject: [PATCH 03/14] Fix code style issues and documentation --- src/zabapgit_2fa.prog.abap | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index 57487717c..0401965a5 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -397,8 +397,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. li_http_client->response->get_status( IMPORTING code = lv_http_code - reason = lv_http_code_description - ). + reason = lv_http_code_description ). IF lv_http_code <> 200. RAISE EXCEPTION TYPE lcx_2fa_auth_failed EXPORTING @@ -428,8 +427,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. li_http_client->response->get_status( IMPORTING code = lv_http_code - reason = lv_http_code_description - ). + reason = lv_http_code_description ). IF lv_http_code <> 201. RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed EXPORTING @@ -471,7 +469,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. APPEND ls_rest_line TO lt_rest_parvalues. " The fingerprint must be unique, otherwise only one token can be generated, unless the user - " deletes it in Github's settings. This is problematic if he deletes it in abapGit but keeps it + " deletes it in GitHub's settings. This is problematic if he deletes it in abapGit but keeps it " on GitHub. lv_fingerprint = |abapGit-{ sy-sysid }-{ sy-uname }-{ sy-datum }-{ sy-uzeit }|. GET REFERENCE OF lv_fingerprint INTO lr_data_ref. @@ -480,7 +478,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. APPEND ls_rest_line TO lt_rest_parvalues. " Dynamic source table is used because otherwise identifiers will always be written in uppercase - " which is not supported by the GitHub's API. + " which is not supported by GitHub's API. CALL TRANSFORMATION id SOURCE (lt_rest_parvalues) RESULT XML lo_json_writer. From 7715bdc59df7b2fcea3abb32a620fa1df4270994 Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 15 Jan 2017 18:35:20 +0100 Subject: [PATCH 04/14] Replace usages of cl_sxml* for compatibility cl_sxml_writer/reader do not support json on AS ABAP 7.02 --- src/zabapgit_2fa.prog.abap | 70 ++++++++++++-------------------------- 1 file changed, 21 insertions(+), 49 deletions(-) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index 0401965a5..ea3f9879a 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -436,80 +436,52 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. rv_access_token = get_token_from_response( li_http_client->response ). IF rv_access_token IS INITIAL. - RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed. + RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed + EXPORTING + iv_error_text = 'Token generation failed: parser error' ##NO_TEXT. ENDIF. ENDMETHOD. METHOD set_access_token_request. - CONSTANTS: BEGIN OF lc_create_access_token_request, - scopes TYPE string VALUE 'repo', - note TYPE string VALUE 'abapGit', - END OF lc_create_access_token_request. - DATA: lo_json_writer TYPE REF TO cl_sxml_string_writer, - lt_scopes TYPE stringtab, - lt_rest_parvalues TYPE abap_trans_srcbind_tab, - ls_rest_line LIKE LINE OF lt_rest_parvalues, - lt_result_parvalues TYPE abap_trans_resbind_tab, - ls_result_line LIKE LINE OF lt_result_parvalues, - lr_data_ref TYPE REF TO data, - lv_note TYPE string, - lv_fingerprint TYPE string. - - lo_json_writer = cl_sxml_string_writer=>create( type = if_sxml=>co_xt_json ). - APPEND lc_create_access_token_request-scopes TO lt_scopes. - - GET REFERENCE OF lc_create_access_token_request-scopes INTO lr_data_ref. - ls_rest_line-name = 'scopes'. - ls_rest_line-value = lr_data_ref. - APPEND ls_rest_line TO lt_rest_parvalues. - - GET REFERENCE OF lc_create_access_token_request-note INTO lr_data_ref. - ls_rest_line-name = 'note'. - ls_rest_line-value = lr_data_ref. - APPEND ls_rest_line TO lt_rest_parvalues. + DATA: lv_fingerprint TYPE string, + lv_json_string TYPE string. " The fingerprint must be unique, otherwise only one token can be generated, unless the user " deletes it in GitHub's settings. This is problematic if he deletes it in abapGit but keeps it " on GitHub. lv_fingerprint = |abapGit-{ sy-sysid }-{ sy-uname }-{ sy-datum }-{ sy-uzeit }|. - GET REFERENCE OF lv_fingerprint INTO lr_data_ref. - ls_rest_line-name = 'fingerprint'. - ls_rest_line-value = lr_data_ref. - APPEND ls_rest_line TO lt_rest_parvalues. - " Dynamic source table is used because otherwise identifiers will always be written in uppercase - " which is not supported by GitHub's API. - CALL TRANSFORMATION id SOURCE (lt_rest_parvalues) - RESULT XML lo_json_writer. + lv_json_string = |\{"scopes":["repo"],"note":"abapGit","fingerprint":"{ lv_fingerprint }"\}|. - ii_request->set_data( lo_json_writer->get_output( ) ). + ii_request->set_data( cl_abap_codepage=>convert_to( lv_json_string ) ). ENDMETHOD. METHOD get_token_from_response. - CONSTANTS: lc_token_field_name TYPE string VALUE 'token'. - DATA: lt_result_parvalues TYPE abap_trans_resbind_tab, - ls_result_line LIKE LINE OF lt_result_parvalues, - lr_data_ref TYPE REF TO data, - lv_binary_response TYPE xstring. + CONSTANTS: lc_search_regex TYPE string VALUE '.*"token":"([^"]*).*$'. + DATA: lv_response TYPE string, + lo_regex TYPE REF TO cl_abap_regex, + lo_matcher TYPE REF TO cl_abap_matcher. - GET REFERENCE OF rv_token INTO lr_data_ref. - ls_result_line-name = lc_token_field_name. - ls_result_line-value = lr_data_ref. - APPEND ls_result_line TO lt_result_parvalues. + lv_response = cl_abap_codepage=>convert_from( ii_response->get_data( ) ). - lv_binary_response = ii_response->get_data( ). + CREATE OBJECT lo_regex + EXPORTING + pattern = lc_search_regex. - CALL TRANSFORMATION id SOURCE XML lv_binary_response - RESULT (lt_result_parvalues). + lo_matcher = lo_regex->create_matcher( text = lv_response ). + IF lo_matcher->match( ) = abap_true. + rv_token = lo_matcher->get_submatch( 1 ). + ENDIF. ENDMETHOD. METHOD parse_repo_from_url. + CONSTANTS: lc_search_regex TYPE string VALUE 'https?:\/\/(www\.)?github.com\/(.*)$'. DATA: lo_regex TYPE REF TO cl_abap_regex, lo_matcher TYPE REF TO cl_abap_matcher. CREATE OBJECT lo_regex EXPORTING - pattern = 'https?:\/\/(www\.)?github.com\/(.*)$'. + pattern = lc_search_regex. lo_matcher = lo_regex->create_matcher( text = iv_url ). IF lo_matcher->match( ) = abap_true. From f8cf8706013ef80fd9309fdbf01fc117760ae7bb Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 15 Jan 2017 19:11:12 +0100 Subject: [PATCH 05/14] Re-enable enter key in authentication dialog --- src/zabapgit.prog.abap | 5 ++++- src/zabapgit_password_dialog.prog.abap | 11 ++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/zabapgit.prog.abap b/src/zabapgit.prog.abap index b81885ed0..a6f7e5758 100644 --- a/src/zabapgit.prog.abap +++ b/src/zabapgit.prog.abap @@ -102,5 +102,8 @@ AT SELECTION-SCREEN ON EXIT-COMMAND. AT SELECTION-SCREEN. IF sy-dynnr = lcl_password_dialog=>dynnr. + IF sscrfields-ucomm IS INITIAL. + sscrfields-ucomm = 'ENTER'. + ENDIF. lcl_password_dialog=>on_screen_event( sscrfields-ucomm ). - ENDIF. + ENDIF. \ No newline at end of file diff --git a/src/zabapgit_password_dialog.prog.abap b/src/zabapgit_password_dialog.prog.abap index b87406890..3c6f04637 100644 --- a/src/zabapgit_password_dialog.prog.abap +++ b/src/zabapgit_password_dialog.prog.abap @@ -73,6 +73,7 @@ CLASS lcl_password_dialog IMPLEMENTATION. CLEAR p_pass. p_url = iv_repo_url. p_user = cv_user. + p_2fat = abap_false. mv_confirm = abap_false. gv_mode = iv_mode. gv_delete_token = abap_false. @@ -187,17 +188,9 @@ CLASS lcl_password_dialog IMPLEMENTATION. gv_delete_token = abap_true. LEAVE TO SCREEN 0. ENDIF. - WHEN 'CRET'. + WHEN 'CRET' OR 'ENTER'. mv_confirm = abap_true. LEAVE TO SCREEN 0. - WHEN OTHERS. "TODO REFACTOR !!! A CLUTCH ! - " This will work unless any new specific logic appear - " for other commands. The problem is that the password dialog - " does not have Enter event (or I don't know how to activate it ;) - " so Enter issues previous command from previous screen - " But for now this works :) Fortunately Esc produces another flow -* mv_confirm = abap_true. -* LEAVE TO SCREEN 0. ENDCASE. ENDMETHOD. "on_screen_event From f93bb681b9897f1adfe20bb6491800f866d6587e Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 15 Jan 2017 19:29:12 +0100 Subject: [PATCH 06/14] Improve exception handling --- src/zabapgit_2fa.prog.abap | 17 ++++++++++++----- src/zabapgit_http.prog.abap | 12 +++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index ea3f9879a..b695acb77 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -135,13 +135,17 @@ INTERFACE lif_2fa_authenticator. "! Get a unique identifier for the service that hosts the repository "! @parameter iv_url | Repository url "! @parameter rv_id | Service id + "! @raising lcx_2fa_unsupported | Url is not supported get_service_id_from_url IMPORTING iv_url TYPE string - RETURNING VALUE(rv_id) TYPE string, + RETURNING VALUE(rv_id) TYPE string + RAISING lcx_2fa_unsupported, "! Check if there is a cached access token (for the current user) "! @parameter iv_url | Repository url "! @parameter rv_available | Token is cached + "! @raising lcx_2fa_unsupported | Url is not supported is_cached_access_token_avail IMPORTING iv_url TYPE string - RETURNING VALUE(rv_available) TYPE abap_bool, + RETURNING VALUE(rv_available) TYPE abap_bool + RAISING lcx_2fa_unsupported, "! Get a cached access token "!

"! Username and password are also parameters to decrypt the token if needed. They must no @@ -152,16 +156,19 @@ INTERFACE lif_2fa_authenticator. "! @parameter iv_password | Password "! @parameter rv_token | Access token "! @raising lcx_2fa_no_cached_token | There is no cached token + "! @raising lcx_2fa_unsupported | Url is not supported get_cached_access_token IMPORTING iv_url TYPE string iv_username TYPE string OPTIONAL iv_password TYPE string OPTIONAL RETURNING VALUE(rv_token) TYPE string - RAISING lcx_2fa_no_cached_token, + RAISING lcx_2fa_no_cached_token + lcx_2fa_unsupported, "! Delete a cached token "! @parameter iv_url | Repository url "! @raising lcx_2fa_cache_deletion_failed | Deletion failed delete_cached_access_token IMPORTING iv_url TYPE string - RAISING lcx_2fa_cache_deletion_failed. + RAISING lcx_2fa_cache_deletion_failed + lcx_2fa_unsupported. ENDINTERFACE. "! Default LIF_2FA-AUTHENTICATOR implememtation @@ -246,7 +253,7 @@ CLASS lcl_2fa_authenticator_base IMPLEMENTATION. lcl_app=>user( )->set_2fa_access_token( iv_service_id = get_service_id_from_url( iv_url ) iv_username = iv_username iv_token = lv_encrypted_token ). - CATCH lcx_exception ##NO_HANDLER. + CATCH lcx_exception lcx_2fa_unsupported ##NO_HANDLER. " Not the biggest of deals if caching the token fails ENDTRY. ENDMETHOD. diff --git a/src/zabapgit_http.prog.abap b/src/zabapgit_http.prog.abap index bca230808..e465b9193 100644 --- a/src/zabapgit_http.prog.abap +++ b/src/zabapgit_http.prog.abap @@ -497,9 +497,11 @@ CLASS lcl_http IMPLEMENTATION. lv_popup_mode = lcl_password_dialog=>gc_modes-user_pass_2fa. ENDIF. - CATCH lcx_2fa_error. - lv_2fa_available = abap_false. - CLEAR lv_access_token. + CATCH lcx_2fa_error INTO lx_error. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = lx_error->get_text( ) + ix_previous = lx_error. ENDTRY. ELSE. lv_popup_mode = lcl_password_dialog=>gc_modes-user_pass. @@ -519,7 +521,7 @@ CLASS lcl_http IMPLEMENTATION. IF lv_popup_requested_token_del = abap_true. TRY. li_authenticator->delete_cached_access_token( lv_service_id ). - CATCH lcx_2fa_cache_deletion_failed INTO lx_error. + CATCH lcx_2fa_cache_deletion_failed lcx_2fa_unsupported INTO lx_error. RAISE EXCEPTION TYPE lcx_exception EXPORTING iv_text = lx_error->get_text( ) @@ -538,7 +540,7 @@ CLASS lcl_http IMPLEMENTATION. lv_access_token = li_authenticator->get_cached_access_token( iv_url = iv_url iv_username = lv_user iv_password = lv_pass ). - CATCH lcx_2fa_no_cached_token INTO lx_error. + CATCH lcx_2fa_no_cached_token lcx_2fa_unsupported INTO lx_error. RAISE EXCEPTION TYPE lcx_exception EXPORTING iv_text = lx_error->get_text( ) From 0026e74d799d640fd134294eaef24693f67632d5 Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sat, 21 Jan 2017 18:41:50 +0100 Subject: [PATCH 07/14] Refactor 2FA to not require access token storage --- src/zabapgit.prog.abap | 3 - src/zabapgit_2fa.prog.abap | 207 ++++++------------------- src/zabapgit_http.prog.abap | 136 ++++++---------- src/zabapgit_password_dialog.prog.abap | 115 +++----------- src/zabapgit_persistence.prog.abap | 92 +---------- 5 files changed, 122 insertions(+), 431 deletions(-) diff --git a/src/zabapgit.prog.abap b/src/zabapgit.prog.abap index a6f7e5758..aaaf89121 100644 --- a/src/zabapgit.prog.abap +++ b/src/zabapgit.prog.abap @@ -102,8 +102,5 @@ AT SELECTION-SCREEN ON EXIT-COMMAND. AT SELECTION-SCREEN. IF sy-dynnr = lcl_password_dialog=>dynnr. - IF sscrfields-ucomm IS INITIAL. - sscrfields-ucomm = 'ENTER'. - ENDIF. lcl_password_dialog=>on_screen_event( sscrfields-ucomm ). ENDIF. \ No newline at end of file diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index b695acb77..f3e8ff8e3 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -74,37 +74,13 @@ CLASS lcx_2fa_unsupported IMPLEMENTATION. ENDMETHOD. ENDCLASS. -CLASS lcx_2fa_no_cached_token DEFINITION INHERITING FROM lcx_2fa_error FINAL. - PROTECTED SECTION. - METHODS: - get_default_text REDEFINITION. -ENDCLASS. - -CLASS lcx_2fa_no_cached_token IMPLEMENTATION. - METHOD get_default_text. - rv_text = 'Cached two factor access token requested but not available.' ##NO_TEXT. - ENDMETHOD. -ENDCLASS. - -CLASS lcx_2fa_cache_deletion_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. - PROTECTED SECTION. - METHODS: - get_default_text REDEFINITION. -ENDCLASS. - -CLASS lcx_2fa_cache_deletion_failed IMPLEMENTATION. - METHOD get_default_text. - rv_text = 'Cache deletion failed.' ##NO_TEXT. - ENDMETHOD. -ENDCLASS. "! Defines a two factor authentication authenticator "!

"! Authenticators support one or multiple services and are able to generate access tokens using the "! service's API using the users username, password and two factor authentication token "! (app/sms/tokengenerator). With these access tokens the user can be authenticated to the service's -"! implementation of the git http api, just like the "normal" password would. The authenticator can -"! also store and retrieve the access token it generated. +"! implementation of the git http api, just like the "normal" password would. "!

"!

"! LCL_2FA_AUTHENTICATOR_REGISTRY can be used to find a suitable implementation for a given @@ -139,42 +115,18 @@ INTERFACE lif_2fa_authenticator. get_service_id_from_url IMPORTING iv_url TYPE string RETURNING VALUE(rv_id) TYPE string RAISING lcx_2fa_unsupported, - "! Check if there is a cached access token (for the current user) - "! @parameter iv_url | Repository url - "! @parameter rv_available | Token is cached - "! @raising lcx_2fa_unsupported | Url is not supported - is_cached_access_token_avail IMPORTING iv_url TYPE string - RETURNING VALUE(rv_available) TYPE abap_bool - RAISING lcx_2fa_unsupported, - "! Get a cached access token - "!

- "! Username and password are also parameters to decrypt the token if needed. They must no - "! necessarily be provided if the used authenticator does not use encryption. - "!

+ "! Check if two factor authentication is required "! @parameter iv_url | Repository url "! @parameter iv_username | Username "! @parameter iv_password | Password - "! @parameter rv_token | Access token - "! @raising lcx_2fa_no_cached_token | There is no cached token - "! @raising lcx_2fa_unsupported | Url is not supported - get_cached_access_token IMPORTING iv_url TYPE string - iv_username TYPE string OPTIONAL - iv_password TYPE string OPTIONAL - RETURNING VALUE(rv_token) TYPE string - RAISING lcx_2fa_no_cached_token - lcx_2fa_unsupported, - "! Delete a cached token - "! @parameter iv_url | Repository url - "! @raising lcx_2fa_cache_deletion_failed | Deletion failed - delete_cached_access_token IMPORTING iv_url TYPE string - RAISING lcx_2fa_cache_deletion_failed - lcx_2fa_unsupported. + "! @parameter rv_required | 2FA is required + is_2fa_required IMPORTING iv_url TYPE string + iv_username TYPE string + iv_password TYPE string + RETURNING VALUE(rv_required) TYPE abap_bool. ENDINTERFACE. "! Default LIF_2FA-AUTHENTICATOR implememtation -"!

-"! This uses the user settings to store cached access tokens and encrypts / decrypts them as needed. -"!

CLASS lcl_2fa_authenticator_base DEFINITION ABSTRACT CREATE PUBLIC. @@ -186,9 +138,7 @@ CLASS lcl_2fa_authenticator_base DEFINITION authenticate FOR lif_2fa_authenticator~authenticate, supports_url FOR lif_2fa_authenticator~supports_url, get_service_id_from_url FOR lif_2fa_authenticator~get_service_id_from_url, - is_cached_access_token_avail FOR lif_2fa_authenticator~is_cached_access_token_avail, - get_cached_access_token FOR lif_2fa_authenticator~get_cached_access_token, - delete_cached_token FOR lif_2fa_authenticator~delete_cached_access_token. + is_2fa_required FOR lif_2fa_authenticator~is_2fa_required. METHODS: "! @parameter iv_supported_url_regex | Regular expression to check if a repository url is "! supported, used for default implementation of @@ -196,24 +146,6 @@ CLASS lcl_2fa_authenticator_base DEFINITION constructor IMPORTING iv_supported_url_regex TYPE clike. PROTECTED SECTION. METHODS: - "! Subclass implementation of LIF_2FA_AUTHENTICATOR=>AUTHENTICATE - "!

- "! The caller will take care of caching the token. - "!

- "! @parameter iv_url | Repository url - "! @parameter iv_username | Username - "! @parameter iv_password | Password - "! @parameter iv_2fa_token | Two factor token - "! @parameter rv_access_token | Generated access token - "! @raising lcx_2fa_auth_failed | Authentication failed - "! @raising lcx_2fa_token_gen_failed | Token generation failed - authenticate_internal ABSTRACT IMPORTING iv_url TYPE string - iv_username TYPE string - iv_password TYPE string - iv_2fa_token TYPE string - RETURNING VALUE(rv_access_token) TYPE string - RAISING lcx_2fa_auth_failed - lcx_2fa_token_gen_failed, "! Helper method to raise class based exception after traditional exception was raised "!

"! sy-msg... must be set right before calling! @@ -233,29 +165,7 @@ CLASS lcl_2fa_authenticator_base IMPLEMENTATION. ENDMETHOD. METHOD authenticate. - DATA: lv_encrypted_token TYPE string. - - rv_access_token = authenticate_internal( iv_url = iv_url - iv_username = iv_username - iv_password = iv_password - iv_2fa_token = iv_2fa_token ). - - " Store the access token, by default in the user settings - - " 1. Encrypt it -* lv_encrypted_token = cl_encryption_helper=>encrypt_symmetric( iv_text = rv_access_token -* iv_key = iv_password ). - " TODO: Find something like the above for symmetric encryption - lv_encrypted_token = rv_access_token. - - " 2. Store it - TRY. - lcl_app=>user( )->set_2fa_access_token( iv_service_id = get_service_id_from_url( iv_url ) - iv_username = iv_username - iv_token = lv_encrypted_token ). - CATCH lcx_exception lcx_2fa_unsupported ##NO_HANDLER. - " Not the biggest of deals if caching the token fails - ENDTRY. + RAISE EXCEPTION TYPE lcx_2fa_auth_failed. " Needs to be overwritten in subclasses ENDMETHOD. METHOD supports_url. @@ -266,56 +176,8 @@ CLASS lcl_2fa_authenticator_base IMPLEMENTATION. rv_id = 'UNKNOWN SERVICE'. " Please overwrite in subclasses ENDMETHOD. - METHOD is_cached_access_token_avail. - DATA: lv_service_id TYPE string. - - lv_service_id = get_service_id_from_url( iv_url ). - - " Default storage location is user settings - TRY. - rv_available = boolc( lcl_app=>user( )->get_2fa_access_token( lv_service_id ) - IS NOT INITIAL ). - CATCH lcx_exception. - rv_available = abap_false. - ENDTRY. - ENDMETHOD. - - METHOD get_cached_access_token. - DATA: lv_access_token_encrypted TYPE string, - lx_error TYPE REF TO cx_root. - - TRY. - lv_access_token_encrypted - = lcl_app=>user( )->get_2fa_access_token( get_service_id_from_url( iv_url ) ). - CATCH lcx_exception INTO lx_error. - RAISE EXCEPTION TYPE lcx_2fa_no_cached_token - EXPORTING - ix_previous = lx_error - iv_error_text = lx_error->get_text( ). - ENDTRY. - - IF lv_access_token_encrypted IS INITIAL. - RAISE EXCEPTION TYPE lcx_2fa_no_cached_token. - ENDIF. - - " TODO: Decryption -* rv_token = cl_encryption_helper=>decrypt_symmetric( iv_encrypted = rv_access_token -* iv_key = iv_password ). - rv_token = lv_access_token_encrypted. - ENDMETHOD. - - METHOD delete_cached_token. - DATA: lx_ex TYPE REF TO cx_root. - - TRY. - " Default storage location is user settings - lcl_app=>user( )->delete_2fa_config( get_service_id_from_url( iv_url ) ). - CATCH lcx_exception INTO lx_ex. - RAISE EXCEPTION TYPE lcx_2fa_cache_deletion_failed - EXPORTING - ix_previous = lx_ex - iv_error_text = |Cache deletion failed: { lx_ex->get_text( ) }|. - ENDTRY. + METHOD is_2fa_required. + rv_required = abap_false. ENDMETHOD. METHOD raise_internal_error_from_sy. @@ -338,11 +200,15 @@ CLASS lcl_2fa_github_authenticator DEFINITION PUBLIC SECTION. METHODS: constructor, - get_service_id_from_url REDEFINITION. + get_service_id_from_url REDEFINITION, + authenticate REDEFINITION, + is_2fa_required REDEFINITION. PROTECTED SECTION. - METHODS: - authenticate_internal REDEFINITION. PRIVATE SECTION. + CONSTANTS: + gc_github_api_url TYPE string VALUE `https://api.github.com/`, + gc_otp_header_name TYPE string VALUE `X-Github-OTP`, + gc_restendpoint_authorizations TYPE string VALUE `/authorizations`. METHODS: set_access_token_request IMPORTING ii_request TYPE REF TO if_http_request iv_repo_name TYPE string, @@ -357,10 +223,8 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. super->constructor( 'https?:\/\/(www\.)?github.com.*$' ). ENDMETHOD. - METHOD authenticate_internal. - CONSTANTS: lc_github_api_url TYPE string VALUE `https://api.github.com/`, - lc_otp_header_name TYPE string VALUE `X-Github-OTP`, - lc_restendpoint_authorizations TYPE string VALUE `/authorizations`. + METHOD authenticate. + DATA: li_http_client TYPE REF TO if_http_client, lv_http_code TYPE i, lv_http_code_description TYPE string, @@ -372,7 +236,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. " 1. Try to login to GitHub API with username, password and 2fa token cl_http_client=>create_by_url( EXPORTING - url = lc_github_api_url + url = gc_github_api_url IMPORTING client = li_http_client EXCEPTIONS @@ -386,7 +250,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. " https://developer.github.com/v3/auth/#working-with-two-factor-authentication li_http_client->propertytype_accept_cookie = if_http_client=>co_enabled. - li_http_client->request->set_header_field( name = lc_otp_header_name value = iv_2fa_token ). + li_http_client->request->set_header_field( name = gc_otp_header_name value = iv_2fa_token ). li_http_client->authenticate( username = iv_username password = iv_password ). li_http_client->propertytype_logon_popup = if_http_client=>co_disabled. @@ -418,7 +282,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. set_access_token_request( ii_request = li_http_client->request iv_repo_name = parse_repo_from_url( iv_url ) ). li_http_client->request->set_header_field( name = if_http_header_fields_sap=>request_uri - value = lc_restendpoint_authorizations ). + value = gc_restendpoint_authorizations ). li_http_client->request->set_method( if_http_request=>co_request_method_post ). li_http_client->send( EXCEPTIONS OTHERS = 1 ). @@ -501,6 +365,29 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. METHOD get_service_id_from_url. rv_id = 'github'. ENDMETHOD. + + METHOD is_2fa_required. + DATA: li_client TYPE REF TO if_http_client, + lv_header_value TYPE string. + + cl_http_client=>create_by_url( + EXPORTING + url = gc_github_api_url + IMPORTING + client = li_client ). + + li_client->propertytype_logon_popup = if_http_client=>co_disabled. + + " Try to authenticate without password, if 2FA is required there will be a specific response + " header + li_client->authenticate( username = iv_username password = iv_password ). + li_client->send( ). + li_client->receive( ). + + IF li_client->response->get_header_field( gc_otp_header_name ) CP 'required*'. + rv_required = abap_true. + ENDIF. + ENDMETHOD. ENDCLASS. "! Static registry class to find LIF_2FA_AUTHENTICATOR instances @@ -566,4 +453,4 @@ CLASS lcl_2fa_authenticator_registry IMPLEMENTATION. CATCH lcx_2fa_unsupported ##NO_HANDLER. ENDTRY. ENDMETHOD. -ENDCLASS. \ No newline at end of file +ENDCLASS. diff --git a/src/zabapgit_http.prog.abap b/src/zabapgit_http.prog.abap index e465b9193..e5231d8eb 100644 --- a/src/zabapgit_http.prog.abap +++ b/src/zabapgit_http.prog.abap @@ -469,84 +469,26 @@ CLASS lcl_http IMPLEMENTATION. METHOD acquire_login_details. - DATA: lv_default_user TYPE string, - lv_user TYPE string, - lv_pass TYPE string, - lv_2fa_token TYPE string, - lv_access_token TYPE string, - lo_digest TYPE REF TO lcl_http_digest, - lv_2fa_available TYPE abap_bool, - li_authenticator TYPE REF TO lif_2fa_authenticator, - lx_error TYPE REF TO cx_root, - lv_popup_mode TYPE lcl_password_dialog=>gty_mode, - lv_popup_requested_token_del TYPE abap_bool, - lv_service_id TYPE string. + DATA: lv_default_user TYPE string, + lv_user TYPE string, + lv_pass TYPE string, + lv_2fa_token TYPE string, + lv_access_token TYPE string, + lo_digest TYPE REF TO lcl_http_digest, + li_authenticator TYPE REF TO lif_2fa_authenticator, + lx_error TYPE REF TO cx_root, + lv_use_2fa TYPE abap_bool. lv_default_user = lcl_app=>user( )->get_repo_username( iv_url ). lv_user = lv_default_user. - - IF lcl_2fa_authenticator_registry=>is_url_supported( iv_url ) = abap_true. - TRY. - li_authenticator = lcl_2fa_authenticator_registry=>get_authenticator_for_url( iv_url ). - lv_service_id = li_authenticator->get_service_id_from_url( iv_url ). - IF li_authenticator->is_cached_access_token_avail( iv_url ) = abap_true. - lv_popup_mode = lcl_password_dialog=>gc_modes-unlock_2fa_token. - ELSE. - lv_popup_mode = lcl_password_dialog=>gc_modes-user_pass_2fa. - ENDIF. - - CATCH lcx_2fa_error INTO lx_error. - RAISE EXCEPTION TYPE lcx_exception - EXPORTING - iv_text = lx_error->get_text( ) - ix_previous = lx_error. - ENDTRY. - ELSE. - lv_popup_mode = lcl_password_dialog=>gc_modes-user_pass. - ENDIF. - lcl_password_dialog=>popup( EXPORTING iv_repo_url = iv_url - iv_mode = lv_popup_mode - IMPORTING - ev_delete_token = lv_popup_requested_token_del CHANGING cv_user = lv_user - cv_pass = lv_pass - cv_2fa_token = lv_2fa_token ). - - IF lv_popup_requested_token_del = abap_true. - TRY. - li_authenticator->delete_cached_access_token( lv_service_id ). - CATCH lcx_2fa_cache_deletion_failed lcx_2fa_unsupported INTO lx_error. - RAISE EXCEPTION TYPE lcx_exception - EXPORTING - iv_text = lx_error->get_text( ) - ix_previous = lx_error. - ENDTRY. - - " Cancel authentication, no credentials were provided. This will cause the next http request - " somewhere up the callstack to result in a 401 error. - RETURN. - ENDIF. - - " Unlock cached access token - IF lv_popup_mode = lcl_password_dialog=>gc_modes-unlock_2fa_token. - TRY. - ASSERT li_authenticator IS BOUND. - lv_access_token = li_authenticator->get_cached_access_token( iv_url = iv_url - iv_username = lv_user - iv_password = lv_pass ). - CATCH lcx_2fa_no_cached_token lcx_2fa_unsupported INTO lx_error. - RAISE EXCEPTION TYPE lcx_exception - EXPORTING - iv_text = lx_error->get_text( ) - ix_previous = lx_error. - ENDTRY. - ENDIF. + cv_pass = lv_pass ). IF lv_user IS INITIAL. lcx_exception=>raise( 'HTTP 401, unauthorized' ). @@ -557,25 +499,47 @@ CLASS lcl_http IMPLEMENTATION. iv_username = lv_user ). ENDIF. - IF lv_access_token IS INITIAL AND lv_2fa_token IS NOT INITIAL. - " There is no cached access token but the user provided a two factor token to generate a new - " access token - TRY. - ASSERT li_authenticator IS BOUND. - lv_access_token = li_authenticator->authenticate( iv_url = iv_url - iv_username = lv_user - iv_password = lv_pass - iv_2fa_token = lv_2fa_token ). - CATCH lcx_2fa_error INTO lx_error. - RAISE EXCEPTION TYPE lcx_exception - EXPORTING - iv_text = lx_error->get_text( ) - ix_previous = lx_error. - ENDTRY. - ENDIF. + " Is the repository hoster supported for using two factor authentication? + TRY. + IF lcl_2fa_authenticator_registry=>is_url_supported( iv_url ) = abap_true. + li_authenticator = lcl_2fa_authenticator_registry=>get_authenticator_for_url( iv_url ). - " If there is an access token by now use that as the password instead because two factor - " authentication was requested. + " Is two factor authentication required for this account? + IF li_authenticator->is_2fa_required( iv_url = iv_url + iv_username = lv_user + iv_password = lv_pass ) = abap_true. + + " Get a 2FA token (app/sms) + CALL FUNCTION 'POPUP_GET_STRING' + EXPORTING + label = 'Two factor auth. token' + IMPORTING + value = lv_2fa_token + okay = lv_use_2fa. + IF lv_use_2fa = abap_false. + lcx_exception=>raise( 'Authentication cancelled' ). + ENDIF. + + " Get a new access token + lv_access_token = li_authenticator->authenticate( iv_url = iv_url + iv_username = lv_user + iv_password = lv_pass + iv_2fa_token = lv_2fa_token ). + + " Delete any old ones + ##TODO. + ENDIF. + ENDIF. + + CATCH lcx_2fa_error INTO lx_error. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = lx_error->get_text( ) + ix_previous = lx_error. + ENDTRY. + + " If there is an access token use that as the password instead because two factor authentication + " is required. IF lv_access_token IS NOT INITIAL. lv_pass = lv_access_token. ENDIF. diff --git a/src/zabapgit_password_dialog.prog.abap b/src/zabapgit_password_dialog.prog.abap index 3c6f04637..354f0d44d 100644 --- a/src/zabapgit_password_dialog.prog.abap +++ b/src/zabapgit_password_dialog.prog.abap @@ -16,13 +16,6 @@ SELECTION-SCREEN BEGIN OF LINE. SELECTION-SCREEN COMMENT 1(10) s_pass FOR FIELD p_pass. PARAMETERS: p_pass TYPE string LOWER CASE VISIBLE LENGTH 40 ##SEL_WRONG. SELECTION-SCREEN END OF LINE. -SELECTION-SCREEN BEGIN OF LINE. -PARAMETERS: p_en2fa TYPE abap_bool DEFAULT abap_false USER-COMMAND u1 MODIF ID m1 AS CHECKBOX. -SELECTION-SCREEN COMMENT 4(6) s_2fat FOR FIELD p_2fat MODIF ID m1. -SELECTION-SCREEN POSITION 12. -PARAMETERS: p_2fat TYPE string LOWER CASE VISIBLE LENGTH 40 MODIF ID m1. -SELECTION-SCREEN END OF LINE. -SELECTION-SCREEN FUNCTION KEY 1. SELECTION-SCREEN END OF SCREEN 1002. *----------------------------------------------------------------------- @@ -31,26 +24,14 @@ SELECTION-SCREEN END OF SCREEN 1002. CLASS lcl_password_dialog DEFINITION FINAL. PUBLIC SECTION. - TYPES: - gty_mode TYPE i. - CONSTANTS: - dynnr TYPE char4 VALUE '1002', - BEGIN OF gc_modes, - user_pass TYPE gty_mode VALUE 1, - user_pass_2fa TYPE gty_mode VALUE 2, - unlock_2fa_token TYPE gty_mode VALUE 3, - END OF gc_modes. + CONSTANTS dynnr TYPE char4 VALUE '1002'. CLASS-METHODS popup IMPORTING - iv_repo_url TYPE string - iv_mode TYPE gty_mode DEFAULT gc_modes-user_pass - EXPORTING - ev_delete_token TYPE abap_bool + iv_repo_url TYPE string CHANGING - cv_user TYPE string - cv_pass TYPE string - cv_2fa_token TYPE string. + cv_user TYPE string + cv_pass TYPE string. CLASS-METHODS on_screen_init. CLASS-METHODS on_screen_output. @@ -59,10 +40,7 @@ CLASS lcl_password_dialog DEFINITION FINAL. iv_ucomm TYPE syucomm. PRIVATE SECTION. - CLASS-DATA: - mv_confirm TYPE abap_bool, - gv_mode TYPE gty_mode, - gv_delete_token TYPE abap_bool. + CLASS-DATA mv_confirm TYPE abap_bool. ENDCLASS. "lcl_password_dialog DEFINITION @@ -73,24 +51,18 @@ CLASS lcl_password_dialog IMPLEMENTATION. CLEAR p_pass. p_url = iv_repo_url. p_user = cv_user. - p_2fat = abap_false. mv_confirm = abap_false. - gv_mode = iv_mode. - gv_delete_token = abap_false. CALL SELECTION-SCREEN dynnr STARTING AT 5 5 ENDING AT 60 8. IF mv_confirm = abap_true. cv_user = p_user. cv_pass = p_pass. - cv_2fa_token = p_2fat. ELSE. - CLEAR: cv_user, cv_pass, cv_2fa_token. + CLEAR: cv_user, cv_pass. ENDIF. - ev_delete_token = gv_delete_token. - - CLEAR: p_url, p_user, p_pass, p_2fat. + CLEAR: p_url, p_user, p_pass. ENDMETHOD. "popup @@ -99,7 +71,6 @@ CLASS lcl_password_dialog IMPLEMENTATION. s_url = 'Repo URL' ##NO_TEXT. s_user = 'User' ##NO_TEXT. s_pass = 'Password' ##NO_TEXT. - s_2fat = '2FA' ##NO_TEXT. ENDMETHOD. "on_screen_init METHOD on_screen_output. @@ -107,51 +78,19 @@ CLASS lcl_password_dialog IMPLEMENTATION. ASSERT sy-dynnr = dynnr. - CLEAR p_2fat. - LOOP AT SCREEN. - CASE screen-name. - WHEN 'P_URL'. - screen-input = '0'. - screen-intensified = '1'. - screen-display_3d = '0'. - MODIFY SCREEN. - - WHEN 'P_PASS'. - screen-invisible = '1'. - MODIFY SCREEN. - - WHEN 'P_USER'. - IF gv_mode = gc_modes-unlock_2fa_token. - screen-input = 0. - MODIFY SCREEN. - ENDIF. - - WHEN 'P_2FAT'. - IF p_en2fa = abap_true. - screen-input = '1'. - ELSE. - screen-input = '0'. - p_2fat = 'Two factor authentication token' ##NO_TEXT. - ENDIF. - MODIFY SCREEN. - - ENDCASE. - - IF screen-group1 = 'M1' AND gv_mode <> gc_modes-user_pass_2fa. - screen-invisible = '1'. + IF screen-name = 'P_URL'. + screen-input = '0'. + screen-intensified = '1'. + screen-display_3d = '0'. + MODIFY SCREEN. + ENDIF. + IF screen-name = 'P_PASS'. + screen-invisible = '1'. MODIFY SCREEN. ENDIF. ENDLOOP. - IF gv_mode = gc_modes-unlock_2fa_token. - s_title = 'Unlock two factor authentication token' ##NO_TEXT. - sscrfields-functxt_01 = 'Remove token' ##NO_TEXT. - ELSE. - s_title = 'Login'. - CLEAR sscrfields-functxt_01. - ENDIF. - " Program RSSYSTDB, GUI Status %_CSP PERFORM set_pf_status IN PROGRAM rsdbrunt IF FOUND. APPEND 'NONE' TO lt_ucomm. "Button Check @@ -163,32 +102,26 @@ CLASS lcl_password_dialog IMPLEMENTATION. TABLES p_exclude = lt_ucomm. - IF p_user IS NOT INITIAL AND p_pass IS NOT INITIAL AND p_en2fa = abap_true. - SET CURSOR FIELD 'P_2FAT'. - ELSEIF p_user IS NOT INITIAL. + IF p_user IS NOT INITIAL. SET CURSOR FIELD 'P_PASS'. ENDIF. ENDMETHOD. "on_screen_output METHOD on_screen_event. - DATA: lv_answer TYPE c. ASSERT sy-dynnr = dynnr. " CRET - F8 " OTHERS - simulate Enter press CASE iv_ucomm. - WHEN 'FC01'. " Delete two factor code - CALL FUNCTION 'POPUP_TO_CONFIRM' - EXPORTING - text_question = 'Do you really want to remove this two factor access token?' - IMPORTING - answer = lv_answer. - IF lv_answer = '1'. - gv_delete_token = abap_true. - LEAVE TO SCREEN 0. - ENDIF. - WHEN 'CRET' OR 'ENTER'. + WHEN 'CRET'. + mv_confirm = abap_true. + WHEN OTHERS. "TODO REFACTOR !!! A CLUTCH ! + " This will work unless any new specific logic appear + " for other commands. The problem is that the password dialog + " does not have Enter event (or I don't know how to activate it ;) + " so Enter issues previous command from previous screen + " But for now this works :) Fortunately Esc produces another flow mv_confirm = abap_true. LEAVE TO SCREEN 0. ENDCASE. diff --git a/src/zabapgit_persistence.prog.abap b/src/zabapgit_persistence.prog.abap index e8e8d96a1..6a383dfbc 100644 --- a/src/zabapgit_persistence.prog.abap +++ b/src/zabapgit_persistence.prog.abap @@ -1,4 +1,4 @@ -*&---------------------------- +*&---------------------------------------------------------------------* *& Include ZABAPGIT_PERSISTENCE *&---------------------------------------------------------------------* @@ -406,21 +406,6 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. RETURNING VALUE(rv_email) TYPE string RAISING lcx_exception. - METHODS get_2fa_access_token - IMPORTING iv_service_id TYPE string - RETURNING VALUE(rv_token) TYPE string - RAISING lcx_exception. - - METHODS set_2fa_access_token - IMPORTING iv_service_id TYPE string - iv_username TYPE string - iv_token TYPE string - RAISING lcx_exception. - - METHODS delete_2fa_config - IMPORTING iv_service_id TYPE string - RAISING lcx_exception. - METHODS toggle_hide_files RETURNING VALUE(rv_hide) TYPE abap_bool RAISING lcx_exception. @@ -470,13 +455,6 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. END OF ty_repo_config. TYPES: ty_repo_config_tt TYPE STANDARD TABLE OF ty_repo_config WITH DEFAULT KEY. - TYPES: BEGIN OF ty_git_hosting_service, - service_id TYPE string, - username TYPE string, - access_token TYPE string, - END OF ty_git_hosting_service. - TYPES: ty_git_hosting_service_tab TYPE STANDARD TABLE OF ty_git_hosting_service WITH DEFAULT KEY. - TYPES: BEGIN OF ty_user, username TYPE string, email TYPE string, @@ -486,7 +464,6 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. changes_only TYPE abap_bool, diff_unified TYPE abap_bool, favorites TYPE tt_favorites, - two_fact_cfg TYPE ty_git_hosting_service_tab, END OF ty_user. METHODS constructor @@ -519,16 +496,6 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. is_repo_config TYPE ty_repo_config RAISING lcx_exception. - METHODS read_2fa_config - IMPORTING iv_service_id TYPE ty_git_hosting_service-service_id - RETURNING VALUE(rs_2fa_config) TYPE ty_git_hosting_service - RAISING lcx_exception. - - METHODS update_2fa_config - IMPORTING iv_service_id TYPE ty_git_hosting_service-service_id - is_2fa_config TYPE ty_git_hosting_service - RAISING lcx_exception. - ENDCLASS. "lcl_persistence_user DEFINITION CLASS lcl_persistence_user IMPLEMENTATION. @@ -672,48 +639,6 @@ CLASS lcl_persistence_user IMPLEMENTATION. ENDMETHOD. "update_repo_config - METHOD read_2fa_config. - DATA: lt_2fa_config TYPE ty_git_hosting_service_tab, - lv_key TYPE string. - - lv_key = to_lower( iv_service_id ). - lt_2fa_config = read( )-two_fact_cfg. - READ TABLE lt_2fa_config INTO rs_2fa_config WITH KEY service_id = lv_key. - ENDMETHOD. - - METHOD update_2fa_config. - DATA: ls_user TYPE ty_user, - lv_key TYPE string. - FIELD-SYMBOLS TYPE ty_git_hosting_service. - - ls_user = read( ). - lv_key = to_lower( iv_service_id ). - - READ TABLE ls_user-two_fact_cfg ASSIGNING WITH KEY service_id = lv_key. - IF sy-subrc IS NOT INITIAL. - APPEND INITIAL LINE TO ls_user-two_fact_cfg ASSIGNING . - ENDIF. - = is_2fa_config. - -service_id = lv_key. - - update( ls_user ). - ENDMETHOD. - - METHOD delete_2fa_config. - DATA: ls_user TYPE ty_user, - lv_key TYPE string. - - ls_user = read( ). - lv_key = to_lower( iv_service_id ). - - DELETE ls_user-two_fact_cfg WHERE service_id = lv_key. - IF sy-subrc <> 0. - lcx_exception=>raise( '2FA config could not be deleted.' ) ##NO_TEXT. - ENDIF. - - update( ls_user ). - ENDMETHOD. - METHOD set_repo_username. DATA: ls_repo_config TYPE ty_repo_config. @@ -746,21 +671,6 @@ CLASS lcl_persistence_user IMPLEMENTATION. ENDMETHOD. "get_repo_email - METHOD get_2fa_access_token. - rv_token = read_2fa_config( iv_service_id )-access_token. - ENDMETHOD. - - METHOD set_2fa_access_token. - DATA: ls_config TYPE ty_git_hosting_service. - - ls_config = read_2fa_config( iv_service_id ). - ls_config-service_id = iv_service_id. - ls_config-username = iv_username. - ls_config-access_token = iv_token. - - update_2fa_config( iv_service_id = iv_service_id is_2fa_config = ls_config ). - ENDMETHOD. - METHOD toggle_hide_files. DATA ls_user TYPE ty_user. From afd1c4e766cf491bbf0cd5afe8cd1eb83df24280 Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 22 Jan 2017 11:26:57 +0100 Subject: [PATCH 08/14] Update 2FA PR for v1.26.0 --- src/zabapgit.prog.abap | 2 +- src/zabapgit_2fa.prog.xml | 23 ----------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/zabapgit.prog.abap b/src/zabapgit.prog.abap index aaaf89121..b81885ed0 100644 --- a/src/zabapgit.prog.abap +++ b/src/zabapgit.prog.abap @@ -103,4 +103,4 @@ AT SELECTION-SCREEN ON EXIT-COMMAND. AT SELECTION-SCREEN. IF sy-dynnr = lcl_password_dialog=>dynnr. lcl_password_dialog=>on_screen_event( sscrfields-ucomm ). - ENDIF. \ No newline at end of file + ENDIF. diff --git a/src/zabapgit_2fa.prog.xml b/src/zabapgit_2fa.prog.xml index 84863cedb..048b86095 100644 --- a/src/zabapgit_2fa.prog.xml +++ b/src/zabapgit_2fa.prog.xml @@ -5,42 +5,19 @@ ZABAPGIT_2FA A - - X S D$ - - - I - - - - 0000-00-00 - - 0000-00-00 - - - - - X - - 0000-00-00 - - 0000-00-00 - D$S X R - ZABAPGIT_2FA 12 - From 8a8df0529012a2d4a2b607923e0a424d6238ab0a Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sat, 4 Feb 2017 21:02:19 +0100 Subject: [PATCH 09/14] Delete previously created 2FA access tokens --- src/zabapgit_2fa.prog.abap | 355 ++++++++++++++++++++++++++++-------- src/zabapgit_http.prog.abap | 58 +----- 2 files changed, 287 insertions(+), 126 deletions(-) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index f3e8ff8e3..1f2ac7e61 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -74,6 +74,18 @@ CLASS lcx_2fa_unsupported IMPLEMENTATION. ENDMETHOD. ENDCLASS. +CLASS lcx_2fa_token_del_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_token_del_failed IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Deleting previous access tokens failed.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + "! Defines a two factor authentication authenticator "!

@@ -123,7 +135,20 @@ INTERFACE lif_2fa_authenticator. is_2fa_required IMPORTING iv_url TYPE string iv_username TYPE string iv_password TYPE string - RETURNING VALUE(rv_required) TYPE abap_bool. + RETURNING VALUE(rv_required) TYPE abap_bool, + "! Delete all previously created access tokens for abapGit + "! @parameter iv_url | Repository url + "! @parameter iv_username | Username + "! @parameter iv_password | Password + "! @parameter iv_2fa_token | Two factor token + "! @raising lcx_2fa_token_del_failed | Token deletion failed + "! @raising lcx_2fa_auth_failed | Authentication failed + delete_access_tokens IMPORTING iv_url TYPE string + iv_username TYPE string + iv_password TYPE string + iv_2fa_token TYPE string + RAISING lcx_2fa_token_del_failed + lcx_2fa_auth_failed. ENDINTERFACE. "! Default LIF_2FA-AUTHENTICATOR implememtation @@ -138,19 +163,20 @@ CLASS lcl_2fa_authenticator_base DEFINITION authenticate FOR lif_2fa_authenticator~authenticate, supports_url FOR lif_2fa_authenticator~supports_url, get_service_id_from_url FOR lif_2fa_authenticator~get_service_id_from_url, - is_2fa_required FOR lif_2fa_authenticator~is_2fa_required. + is_2fa_required FOR lif_2fa_authenticator~is_2fa_required, + delete_access_tokens FOR lif_2fa_authenticator~delete_access_tokens. METHODS: "! @parameter iv_supported_url_regex | Regular expression to check if a repository url is "! supported, used for default implementation of "! SUPPORTS_URL constructor IMPORTING iv_supported_url_regex TYPE clike. PROTECTED SECTION. - METHODS: + CLASS-METHODS: "! Helper method to raise class based exception after traditional exception was raised "!

"! sy-msg... must be set right before calling! "!

- raise_internal_error_from_sy FINAL RAISING lcx_2fa_auth_failed. + raise_internal_error_from_sy RAISING lcx_2fa_auth_failed. PRIVATE SECTION. DATA: mo_url_regex TYPE REF TO cl_abap_regex. @@ -180,6 +206,10 @@ CLASS lcl_2fa_authenticator_base IMPLEMENTATION. rv_required = abap_false. ENDMETHOD. + METHOD delete_access_tokens. + RAISE EXCEPTION TYPE lcx_2fa_token_del_failed. " Needs to be overwritten in subclasses + ENDMETHOD. + METHOD raise_internal_error_from_sy. DATA: lv_error_msg TYPE string. @@ -202,20 +232,30 @@ CLASS lcl_2fa_github_authenticator DEFINITION constructor, get_service_id_from_url REDEFINITION, authenticate REDEFINITION, - is_2fa_required REDEFINITION. + is_2fa_required REDEFINITION, + delete_access_tokens REDEFINITION. PROTECTED SECTION. PRIVATE SECTION. CONSTANTS: gc_github_api_url TYPE string VALUE `https://api.github.com/`, gc_otp_header_name TYPE string VALUE `X-Github-OTP`, gc_restendpoint_authorizations TYPE string VALUE `/authorizations`. - METHODS: - set_access_token_request IMPORTING ii_request TYPE REF TO if_http_request - iv_repo_name TYPE string, + CLASS-METHODS: + set_new_token_request IMPORTING ii_request TYPE REF TO if_http_request, get_token_from_response IMPORTING ii_response TYPE REF TO if_http_response RETURNING VALUE(rv_token) TYPE string, parse_repo_from_url IMPORTING iv_url TYPE string - RETURNING VALUE(rv_repo_name) TYPE string. + RETURNING VALUE(rv_repo_name) TYPE string, + set_list_token_request IMPORTING ii_request TYPE REF TO if_http_request, + get_tobedel_tokens_from_resp IMPORTING ii_response TYPE REF TO if_http_response + RETURNING VALUE(rt_ids) TYPE stringtab, + set_del_token_request IMPORTING ii_request TYPE REF TO if_http_request + iv_token_id TYPE string, + get_authenticated_client IMPORTING iv_username TYPE string + iv_password TYPE string + iv_2fa_token TYPE string + RETURNING VALUE(ri_client) TYPE REF TO if_http_client + RAISING lcx_2fa_auth_failed. ENDCLASS. CLASS lcl_2fa_github_authenticator IMPLEMENTATION. @@ -224,66 +264,19 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ENDMETHOD. METHOD authenticate. - DATA: li_http_client TYPE REF TO if_http_client, lv_http_code TYPE i, - lv_http_code_description TYPE string, - lv_binary_response TYPE xstring, - BEGIN OF ls_success_response, - token TYPE string, - END OF ls_success_response. - - " 1. Try to login to GitHub API with username, password and 2fa token - cl_http_client=>create_by_url( - EXPORTING - url = gc_github_api_url - IMPORTING - client = li_http_client - EXCEPTIONS - argument_not_found = 1 - plugin_not_active = 2 - internal_error = 3 - OTHERS = 4 ). - IF sy-subrc <> 0. - raise_internal_error_from_sy( ). - ENDIF. - - " https://developer.github.com/v3/auth/#working-with-two-factor-authentication - li_http_client->propertytype_accept_cookie = if_http_client=>co_enabled. - li_http_client->request->set_header_field( name = gc_otp_header_name value = iv_2fa_token ). - li_http_client->authenticate( username = iv_username password = iv_password ). - li_http_client->propertytype_logon_popup = if_http_client=>co_disabled. - - li_http_client->send( EXCEPTIONS OTHERS = 1 ). - IF sy-subrc <> 0. - raise_internal_error_from_sy( ). - ENDIF. - - li_http_client->receive( EXCEPTIONS OTHERS = 1 ). - IF sy-subrc <> 0. - raise_internal_error_from_sy( ). - ENDIF. - - " Check if authentication has succeeded - li_http_client->response->get_status( - IMPORTING - code = lv_http_code - reason = lv_http_code_description ). - IF lv_http_code <> 200. - RAISE EXCEPTION TYPE lcx_2fa_auth_failed - EXPORTING - iv_error_text = |Authentication failed: { lv_http_code_description }|. - ENDIF. + lv_http_code_description TYPE string. + " 1. Try to login to GitHub API + li_http_client = get_authenticated_client( iv_username = iv_username + iv_password = iv_password + iv_2fa_token = iv_2fa_token ). " 2. Create an access token which can be used instead of a password " https://developer.github.com/v3/oauth_authorizations/#create-a-new-authorization - set_access_token_request( ii_request = li_http_client->request - iv_repo_name = parse_repo_from_url( iv_url ) ). - li_http_client->request->set_header_field( name = if_http_header_fields_sap=>request_uri - value = gc_restendpoint_authorizations ). - li_http_client->request->set_method( if_http_request=>co_request_method_post ). + set_new_token_request( ii_request = li_http_client->request ). li_http_client->send( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. @@ -313,22 +306,37 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ENDIF. ENDMETHOD. - METHOD set_access_token_request. - DATA: lv_fingerprint TYPE string, - lv_json_string TYPE string. + METHOD set_new_token_request. + DATA: lv_json_string TYPE string. - " The fingerprint must be unique, otherwise only one token can be generated, unless the user - " deletes it in GitHub's settings. This is problematic if he deletes it in abapGit but keeps it - " on GitHub. - lv_fingerprint = |abapGit-{ sy-sysid }-{ sy-uname }-{ sy-datum }-{ sy-uzeit }|. - - lv_json_string = |\{"scopes":["repo"],"note":"abapGit","fingerprint":"{ lv_fingerprint }"\}|. + lv_json_string = |\{"scopes":["repo"],"note":"abapGit","fingerprint":"abapGit2FA"\}|. ii_request->set_data( cl_abap_codepage=>convert_to( lv_json_string ) ). + ii_request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = gc_restendpoint_authorizations ). + ii_request->set_method( if_http_request=>co_request_method_post ). + ENDMETHOD. + + METHOD set_list_token_request. + ii_request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = gc_restendpoint_authorizations ). + ii_request->set_method( if_http_request=>co_request_method_get ). + ENDMETHOD. + + METHOD set_del_token_request. + DATA: lv_url TYPE string. + + lv_url = |{ gc_restendpoint_authorizations }/{ iv_token_id }|. + + ii_request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = lv_url ). + " Other methods than POST and GET do not have constants unfortunately + " ii_request->set_method( if_http_request=>co_request_method_delete ). + ii_request->set_method( 'DELETE' ). ENDMETHOD. METHOD get_token_from_response. - CONSTANTS: lc_search_regex TYPE string VALUE '.*"token":"([^"]*).*$'. + CONSTANTS: lc_search_regex TYPE string VALUE `.*"token":"([^"]*).*$`. DATA: lv_response TYPE string, lo_regex TYPE REF TO cl_abap_regex, lo_matcher TYPE REF TO cl_abap_matcher. @@ -345,6 +353,26 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ENDIF. ENDMETHOD. + METHOD get_tobedel_tokens_from_resp. + CONSTANTS: lc_search_regex TYPE string + VALUE `\{"id": ?(\d+)[^\{]*"app":\{[^\{^\}]*\}[^\{]*"fingerprint": ?` & + `"abapGit2FA"[^\{]*\}`. + DATA: lv_response TYPE string, + lo_regex TYPE REF TO cl_abap_regex, + lo_matcher TYPE REF TO cl_abap_matcher. + + lv_response = cl_abap_codepage=>convert_from( ii_response->get_data( ) ). + + CREATE OBJECT lo_regex + EXPORTING + pattern = lc_search_regex. + + lo_matcher = lo_regex->create_matcher( text = lv_response ). + WHILE lo_matcher->find_next( ) = abap_true. + APPEND lo_matcher->get_submatch( 1 ) TO rt_ids. + ENDWHILE. + ENDMETHOD. + METHOD parse_repo_from_url. CONSTANTS: lc_search_regex TYPE string VALUE 'https?:\/\/(www\.)?github.com\/(.*)$'. DATA: lo_regex TYPE REF TO cl_abap_regex, @@ -367,7 +395,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ENDMETHOD. METHOD is_2fa_required. - DATA: li_client TYPE REF TO if_http_client, + DATA: li_client TYPE REF TO if_http_client, lv_header_value TYPE string. cl_http_client=>create_by_url( @@ -378,8 +406,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. li_client->propertytype_logon_popup = if_http_client=>co_disabled. - " Try to authenticate without password, if 2FA is required there will be a specific response - " header + " Try to authenticate, if 2FA is required there will be a specific response header li_client->authenticate( username = iv_username password = iv_password ). li_client->send( ). li_client->receive( ). @@ -388,6 +415,113 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. rv_required = abap_true. ENDIF. ENDMETHOD. + + METHOD delete_access_tokens. + DATA: li_http_client TYPE REF TO if_http_client, + lv_http_code TYPE i, + lv_http_code_description TYPE string, + lt_tobedeleted_tokens TYPE stringtab. + FIELD-SYMBOLS: TYPE string. + + li_http_client = get_authenticated_client( iv_username = iv_username + iv_password = iv_password + iv_2fa_token = iv_2fa_token ). + + set_list_token_request( li_http_client->request ). + li_http_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + li_http_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + li_http_client->response->get_status( + IMPORTING + code = lv_http_code + reason = lv_http_code_description ). + IF lv_http_code <> 200. + RAISE EXCEPTION TYPE lcx_2fa_token_del_failed + EXPORTING + iv_error_text = |Could not fetch current 2FA authorizations: | && + |{ lv_http_code } { lv_http_code_description }|. + ENDIF. + + lt_tobedeleted_tokens = get_tobedel_tokens_from_resp( li_http_client->response ). + LOOP AT lt_tobedeleted_tokens ASSIGNING WHERE table_line IS NOT INITIAL. + set_del_token_request( ii_request = li_http_client->request + iv_token_id = ). + li_http_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + li_http_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + li_http_client->response->get_status( + IMPORTING + code = lv_http_code + reason = lv_http_code_description ). + IF lv_http_code <> 204. + RAISE EXCEPTION TYPE lcx_2fa_token_del_failed + EXPORTING + iv_error_text = |Could not delete token '{ }': | && + |{ lv_http_code } { lv_http_code_description }|. + ENDIF. + ENDLOOP. + ENDMETHOD. + + METHOD get_authenticated_client. + DATA: lv_http_code TYPE i, + lv_http_code_description TYPE string. + + " Try to login to GitHub API with username, password and 2fa token + cl_http_client=>create_by_url( + EXPORTING + url = gc_github_api_url + IMPORTING + client = ri_client + EXCEPTIONS + argument_not_found = 1 + plugin_not_active = 2 + internal_error = 3 + OTHERS = 4 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + " https://developer.github.com/v3/auth/#working-with-two-factor-authentication + ri_client->propertytype_accept_cookie = if_http_client=>co_enabled. + ri_client->request->set_header_field( name = gc_otp_header_name value = iv_2fa_token ). + ri_client->authenticate( username = iv_username password = iv_password ). + ri_client->propertytype_logon_popup = if_http_client=>co_disabled. + + ri_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + ri_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + " Check if authentication has succeeded + ri_client->response->get_status( + IMPORTING + code = lv_http_code + reason = lv_http_code_description ). + IF lv_http_code <> 200. + RAISE EXCEPTION TYPE lcx_2fa_auth_failed + EXPORTING + iv_error_text = |Authentication failed: { lv_http_code_description }|. + ENDIF. + ENDMETHOD. ENDCLASS. "! Static registry class to find LIF_2FA_AUTHENTICATOR instances @@ -409,7 +543,22 @@ CLASS lcl_2fa_authenticator_registry DEFINITION "! @parameter iv_url | Url of the repository / service "! @parameter rv_supported | 2FA is supported is_url_supported IMPORTING iv_url TYPE string - RETURNING VALUE(rv_supported) TYPE abap_bool. + RETURNING VALUE(rv_supported) TYPE abap_bool, + "! Offer to use two factor authentication if supported and required + "!

+ "! This uses GUI functionality to display a popup to request the user to enter a two factor + "! token. Also an dummy authentication request might be used to find out if two factor + "! authentication is required for the account. + "!

+ "! @parameter iv_url | Url of the repository / service + "! @parameter cv_username | Username + "! @parameter cv_password | Password, will be replaced by an access token if two factor + "! authentication succeeds + "! @raising lcx_exception | Error in two factor authentication + use_2fa_if_required IMPORTING iv_url TYPE string + CHANGING cv_username TYPE string + cv_password TYPE string + RAISING lcx_exception. CLASS-DATA: "! All authenticators managed by the registry gt_registered_authenticators TYPE HASHED TABLE OF REF TO lif_2fa_authenticator @@ -453,4 +602,58 @@ CLASS lcl_2fa_authenticator_registry IMPLEMENTATION. CATCH lcx_2fa_unsupported ##NO_HANDLER. ENDTRY. ENDMETHOD. + + METHOD use_2fa_if_required. + DATA: li_authenticator TYPE REF TO lif_2fa_authenticator, + lv_2fa_token TYPE string, + lv_use_2fa TYPE abap_bool, + lv_access_token TYPE string, + lx_ex TYPE REF TO cx_root. + + IF is_url_supported( iv_url ) = abap_false. + RETURN. + ENDIF. + + TRY. + li_authenticator = get_authenticator_for_url( iv_url ). + + " Is two factor authentication required for this account? + IF li_authenticator->is_2fa_required( iv_url = iv_url + iv_username = cv_username + iv_password = cv_password ) = abap_true. + + " Get a 2FA token (app/sms) + CALL FUNCTION 'POPUP_GET_STRING' + EXPORTING + label = 'Two factor auth. token' + IMPORTING + value = lv_2fa_token + okay = lv_use_2fa. + IF lv_use_2fa = abap_false. + lcx_exception=>raise( 'Authentication cancelled' ). + ENDIF. + + " Delete an old access token if it exists + li_authenticator->delete_access_tokens( iv_url = iv_url + iv_username = cv_username + iv_password = cv_password + iv_2fa_token = lv_2fa_token ). + + " Get a new access token + lv_access_token = li_authenticator->authenticate( iv_url = iv_url + iv_username = cv_username + iv_password = cv_password + iv_2fa_token = lv_2fa_token ). + + " Use the access token instead of the password + cv_password = lv_access_token. + ENDIF. + + CATCH lcx_2fa_error INTO lx_ex. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = |2FA error: { lx_ex->get_text( ) }| + ix_previous = lx_ex. + ENDTRY. + ENDMETHOD. ENDCLASS. diff --git a/src/zabapgit_http.prog.abap b/src/zabapgit_http.prog.abap index e5231d8eb..b8e12a738 100644 --- a/src/zabapgit_http.prog.abap +++ b/src/zabapgit_http.prog.abap @@ -472,12 +472,7 @@ CLASS lcl_http IMPLEMENTATION. DATA: lv_default_user TYPE string, lv_user TYPE string, lv_pass TYPE string, - lv_2fa_token TYPE string, - lv_access_token TYPE string, - lo_digest TYPE REF TO lcl_http_digest, - li_authenticator TYPE REF TO lif_2fa_authenticator, - lx_error TYPE REF TO cx_root, - lv_use_2fa TYPE abap_bool. + lo_digest TYPE REF TO lcl_http_digest. lv_default_user = lcl_app=>user( )->get_repo_username( iv_url ). @@ -499,50 +494,13 @@ CLASS lcl_http IMPLEMENTATION. iv_username = lv_user ). ENDIF. - " Is the repository hoster supported for using two factor authentication? - TRY. - IF lcl_2fa_authenticator_registry=>is_url_supported( iv_url ) = abap_true. - li_authenticator = lcl_2fa_authenticator_registry=>get_authenticator_for_url( iv_url ). - - " Is two factor authentication required for this account? - IF li_authenticator->is_2fa_required( iv_url = iv_url - iv_username = lv_user - iv_password = lv_pass ) = abap_true. - - " Get a 2FA token (app/sms) - CALL FUNCTION 'POPUP_GET_STRING' - EXPORTING - label = 'Two factor auth. token' - IMPORTING - value = lv_2fa_token - okay = lv_use_2fa. - IF lv_use_2fa = abap_false. - lcx_exception=>raise( 'Authentication cancelled' ). - ENDIF. - - " Get a new access token - lv_access_token = li_authenticator->authenticate( iv_url = iv_url - iv_username = lv_user - iv_password = lv_pass - iv_2fa_token = lv_2fa_token ). - - " Delete any old ones - ##TODO. - ENDIF. - ENDIF. - - CATCH lcx_2fa_error INTO lx_error. - RAISE EXCEPTION TYPE lcx_exception - EXPORTING - iv_text = lx_error->get_text( ) - ix_previous = lx_error. - ENDTRY. - - " If there is an access token use that as the password instead because two factor authentication - " is required. - IF lv_access_token IS NOT INITIAL. - lv_pass = lv_access_token. - ENDIF. + " Offer two factor authentication if it is available and required + lcl_2fa_authenticator_registry=>use_2fa_if_required( + EXPORTING + iv_url = iv_url + CHANGING + cv_username = lv_user + cv_password = lv_pass ). rv_scheme = ii_client->response->get_header_field( 'www-authenticate' ). FIND REGEX '^(\w+)' IN rv_scheme SUBMATCHES rv_scheme. From d7fcf8a1c9e5ce98fade8c7368a9331e47f25bbb Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 5 Feb 2017 12:47:59 +0100 Subject: [PATCH 10/14] Apply requested code review changes --- src/zabapgit_2fa.prog.abap | 99 +++++++++++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index 1f2ac7e61..be267d9d2 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -86,6 +86,18 @@ CLASS lcx_2fa_token_del_failed IMPLEMENTATION. ENDMETHOD. ENDCLASS. +CLASS lcx_2fa_communication_error DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_communication_error IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Communication error.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + "! Defines a two factor authentication authenticator "!

@@ -114,7 +126,8 @@ INTERFACE lif_2fa_authenticator. iv_2fa_token TYPE string RETURNING VALUE(rv_access_token) TYPE string RAISING lcx_2fa_auth_failed - lcx_2fa_token_gen_failed, + lcx_2fa_token_gen_failed + lcx_2fa_communication_error, "! Check if this authenticator instance supports the give repository url "! @parameter iv_url | Repository url "! @parameter rv_supported | Is supported @@ -135,7 +148,8 @@ INTERFACE lif_2fa_authenticator. is_2fa_required IMPORTING iv_url TYPE string iv_username TYPE string iv_password TYPE string - RETURNING VALUE(rv_required) TYPE abap_bool, + RETURNING VALUE(rv_required) TYPE abap_bool + RAISING lcx_2fa_communication_error, "! Delete all previously created access tokens for abapGit "! @parameter iv_url | Repository url "! @parameter iv_username | Username @@ -148,6 +162,7 @@ INTERFACE lif_2fa_authenticator. iv_password TYPE string iv_2fa_token TYPE string RAISING lcx_2fa_token_del_failed + lcx_2fa_communication_error lcx_2fa_auth_failed. ENDINTERFACE. @@ -176,7 +191,7 @@ CLASS lcl_2fa_authenticator_base DEFINITION "!

"! sy-msg... must be set right before calling! "!

- raise_internal_error_from_sy RAISING lcx_2fa_auth_failed. + raise_comm_error_from_sy RAISING lcx_2fa_communication_error. PRIVATE SECTION. DATA: mo_url_regex TYPE REF TO cl_abap_regex. @@ -210,15 +225,15 @@ CLASS lcl_2fa_authenticator_base IMPLEMENTATION. RAISE EXCEPTION TYPE lcx_2fa_token_del_failed. " Needs to be overwritten in subclasses ENDMETHOD. - METHOD raise_internal_error_from_sy. + METHOD raise_comm_error_from_sy. DATA: lv_error_msg TYPE string. MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 INTO lv_error_msg. - RAISE EXCEPTION TYPE lcx_2fa_auth_failed + RAISE EXCEPTION TYPE lcx_2fa_communication_error EXPORTING - iv_error_text = |Internal error: { lv_error_msg }| ##NO_TEXT. + iv_error_text = |Communication error: { lv_error_msg }| ##NO_TEXT. ENDMETHOD. ENDCLASS. @@ -255,12 +270,13 @@ CLASS lcl_2fa_github_authenticator DEFINITION iv_password TYPE string iv_2fa_token TYPE string RETURNING VALUE(ri_client) TYPE REF TO if_http_client - RAISING lcx_2fa_auth_failed. + RAISING lcx_2fa_auth_failed + lcx_2fa_communication_error. ENDCLASS. CLASS lcl_2fa_github_authenticator IMPLEMENTATION. METHOD constructor. - super->constructor( 'https?:\/\/(www\.)?github.com.*$' ). + super->constructor( '^https?://(www\.)?github.com.*$' ). ENDMETHOD. METHOD authenticate. @@ -280,12 +296,12 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. li_http_client->send( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. li_http_client->receive( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. li_http_client->response->get_status( @@ -396,20 +412,50 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. METHOD is_2fa_required. DATA: li_client TYPE REF TO if_http_client, - lv_header_value TYPE string. + lv_header_value TYPE string, + lo_settings TYPE REF TO lcl_settings. + + lo_settings = lcl_app=>settings( )->read( ). cl_http_client=>create_by_url( EXPORTING - url = gc_github_api_url + url = gc_github_api_url + ssl_id = 'ANONYM' + proxy_host = lo_settings->get_proxy_url( ) + proxy_service = lo_settings->get_proxy_port( ) IMPORTING - client = li_client ). + client = li_client + EXCEPTIONS + argument_not_found = 1 + plugin_not_active = 2 + internal_error = 3 + OTHERS = 4 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. li_client->propertytype_logon_popup = if_http_client=>co_disabled. + " The request needs to use something other than GET and it needs to be send to an endpoint + " to trigger a SMS. + li_client->request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = gc_restendpoint_authorizations ). + li_client->request->set_method( if_http_request=>co_request_method_post ). + " Try to authenticate, if 2FA is required there will be a specific response header li_client->authenticate( username = iv_username password = iv_password ). - li_client->send( ). - li_client->receive( ). + + li_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + li_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + " The response will either be UNAUTHORIZED or MALFORMED which is both fine. IF li_client->response->get_header_field( gc_otp_header_name ) CP 'required*'. rv_required = abap_true. @@ -430,12 +476,12 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. set_list_token_request( li_http_client->request ). li_http_client->send( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. li_http_client->receive( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. li_http_client->response->get_status( @@ -455,12 +501,12 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. iv_token_id = ). li_http_client->send( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. li_http_client->receive( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. li_http_client->response->get_status( @@ -478,12 +524,19 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. METHOD get_authenticated_client. DATA: lv_http_code TYPE i, - lv_http_code_description TYPE string. + lv_http_code_description TYPE string, + lo_settings TYPE REF TO lcl_settings. " Try to login to GitHub API with username, password and 2fa token + + lo_settings = lcl_app=>settings( )->read( ). + cl_http_client=>create_by_url( EXPORTING url = gc_github_api_url + ssl_id = 'ANONYM' + proxy_host = lo_settings->get_proxy_url( ) + proxy_service = lo_settings->get_proxy_port( ) IMPORTING client = ri_client EXCEPTIONS @@ -492,7 +545,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. internal_error = 3 OTHERS = 4 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. " https://developer.github.com/v3/auth/#working-with-two-factor-authentication @@ -503,12 +556,12 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ri_client->send( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. ri_client->receive( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. - raise_internal_error_from_sy( ). + raise_comm_error_from_sy( ). ENDIF. " Check if authentication has succeeded From b063e35c492d957150e15c2d3783f75a38e17117 Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 5 Feb 2017 13:34:02 +0100 Subject: [PATCH 11/14] Cache authenticated session for one time SMS token --- src/zabapgit_2fa.prog.abap | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index be267d9d2..6d9f7afd8 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -523,10 +523,20 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ENDMETHOD. METHOD get_authenticated_client. + STATICS: BEGIN OF ss_cached_client, + username TYPE string, + client TYPE REF TO if_http_client, + END OF ss_cached_client. DATA: lv_http_code TYPE i, lv_http_code_description TYPE string, lo_settings TYPE REF TO lcl_settings. + " If there is a cached client for the same user return it instead + IF ss_cached_client-client IS BOUND AND ss_cached_client-username = iv_username. + ri_client = ss_cached_client-client. + RETURN. + ENDIF. + " Try to login to GitHub API with username, password and 2fa token lo_settings = lcl_app=>settings( )->read( ). @@ -574,6 +584,10 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. EXPORTING iv_error_text = |Authentication failed: { lv_http_code_description }|. ENDIF. + + " Cache the authenticated http session / client to avoid unnecessary additional authentication + ss_cached_client-username = iv_username. + ss_cached_client-client = ri_client. ENDMETHOD. ENDCLASS. From 991ff4cda6f5c11e546d635c45339521a87adffd Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 5 Feb 2017 13:39:31 +0100 Subject: [PATCH 12/14] Change generated access token description --- src/zabapgit_2fa.prog.abap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index 6d9f7afd8..23690b9f3 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -325,7 +325,7 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. METHOD set_new_token_request. DATA: lv_json_string TYPE string. - lv_json_string = |\{"scopes":["repo"],"note":"abapGit","fingerprint":"abapGit2FA"\}|. + lv_json_string = `{"scopes":["repo"],"note":"Generated by abapGit","fingerprint":"abapGit2FA"}`. ii_request->set_data( cl_abap_codepage=>convert_to( lv_json_string ) ). ii_request->set_header_field( name = if_http_header_fields_sap=>request_uri From 6d630194ae2ad3e79af61fd1ec279caf04a080bf Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 5 Feb 2017 16:36:07 +0100 Subject: [PATCH 13/14] Refactor 2FA internal http session caching --- src/zabapgit_2fa.prog.abap | 91 ++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index 23690b9f3..329b44a4d 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -98,6 +98,17 @@ CLASS lcx_2fa_communication_error IMPLEMENTATION. ENDMETHOD. ENDCLASS. +CLASS lcx_2fa_illegal_state DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_illegal_state IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Illegal state.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. "! Defines a two factor authentication authenticator "!

@@ -110,6 +121,12 @@ ENDCLASS. "! LCL_2FA_AUTHENTICATOR_REGISTRY can be used to find a suitable implementation for a given "! repository. "!

+"!

+"! Using the begin and end methods an internal session can be started and +"! completed in which internal state necessary for multiple methods will be cached. This can be +"! used to avoid having multiple http sessions between authenticate and +"! delete_access_tokens. +"!

INTERFACE lif_2fa_authenticator. METHODS: "! Generate an access token @@ -163,7 +180,13 @@ INTERFACE lif_2fa_authenticator. iv_2fa_token TYPE string RAISING lcx_2fa_token_del_failed lcx_2fa_communication_error - lcx_2fa_auth_failed. + lcx_2fa_auth_failed, + "! Begin an authenticator session that uses internal caching for authorizations + "! @raising lcx_2fa_illegal_state | Session already started + begin RAISING lcx_2fa_illegal_state, + "! End an authenticator session and clear internal caches + "! @raising lcx_2fa_illegal_state | Session not running + end RAISING lcx_2fa_illegal_state. ENDINTERFACE. "! Default LIF_2FA-AUTHENTICATOR implememtation @@ -179,7 +202,9 @@ CLASS lcl_2fa_authenticator_base DEFINITION supports_url FOR lif_2fa_authenticator~supports_url, get_service_id_from_url FOR lif_2fa_authenticator~get_service_id_from_url, is_2fa_required FOR lif_2fa_authenticator~is_2fa_required, - delete_access_tokens FOR lif_2fa_authenticator~delete_access_tokens. + delete_access_tokens FOR lif_2fa_authenticator~delete_access_tokens, + begin FOR lif_2fa_authenticator~begin, + end FOR lif_2fa_authenticator~end. METHODS: "! @parameter iv_supported_url_regex | Regular expression to check if a repository url is "! supported, used for default implementation of @@ -192,9 +217,13 @@ CLASS lcl_2fa_authenticator_base DEFINITION "! sy-msg... must be set right before calling! "!

raise_comm_error_from_sy RAISING lcx_2fa_communication_error. + METHODS: + "! @parameter rv_running | Internal session is currently active + is_session_running RETURNING VALUE(rv_running) TYPE abap_bool. PRIVATE SECTION. DATA: - mo_url_regex TYPE REF TO cl_abap_regex. + mo_url_regex TYPE REF TO cl_abap_regex, + mv_session_running TYPE abap_bool. ENDCLASS. CLASS lcl_2fa_authenticator_base IMPLEMENTATION. @@ -235,6 +264,26 @@ CLASS lcl_2fa_authenticator_base IMPLEMENTATION. EXPORTING iv_error_text = |Communication error: { lv_error_msg }| ##NO_TEXT. ENDMETHOD. + + METHOD begin. + IF mv_session_running = abap_true. + RAISE EXCEPTION TYPE lcx_2fa_illegal_state. + ENDIF. + + mv_session_running = abap_true. + ENDMETHOD. + + METHOD end. + IF mv_session_running = abap_false. + RAISE EXCEPTION TYPE lcx_2fa_illegal_state. + ENDIF. + + mv_session_running = abap_false. + ENDMETHOD. + + METHOD is_session_running. + rv_running = mv_session_running. + ENDMETHOD. ENDCLASS. CLASS lcl_2fa_github_authenticator DEFINITION @@ -248,7 +297,8 @@ CLASS lcl_2fa_github_authenticator DEFINITION get_service_id_from_url REDEFINITION, authenticate REDEFINITION, is_2fa_required REDEFINITION, - delete_access_tokens REDEFINITION. + delete_access_tokens REDEFINITION, + end REDEFINITION. PROTECTED SECTION. PRIVATE SECTION. CONSTANTS: @@ -265,13 +315,16 @@ CLASS lcl_2fa_github_authenticator DEFINITION get_tobedel_tokens_from_resp IMPORTING ii_response TYPE REF TO if_http_response RETURNING VALUE(rt_ids) TYPE stringtab, set_del_token_request IMPORTING ii_request TYPE REF TO if_http_request - iv_token_id TYPE string, + iv_token_id TYPE string. + METHODS: get_authenticated_client IMPORTING iv_username TYPE string iv_password TYPE string iv_2fa_token TYPE string RETURNING VALUE(ri_client) TYPE REF TO if_http_client RAISING lcx_2fa_auth_failed lcx_2fa_communication_error. + DATA: + mi_authenticated_session TYPE REF TO if_http_client. ENDCLASS. CLASS lcl_2fa_github_authenticator IMPLEMENTATION. @@ -523,17 +576,13 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ENDMETHOD. METHOD get_authenticated_client. - STATICS: BEGIN OF ss_cached_client, - username TYPE string, - client TYPE REF TO if_http_client, - END OF ss_cached_client. DATA: lv_http_code TYPE i, lv_http_code_description TYPE string, lo_settings TYPE REF TO lcl_settings. - " If there is a cached client for the same user return it instead - IF ss_cached_client-client IS BOUND AND ss_cached_client-username = iv_username. - ri_client = ss_cached_client-client. + " If there is a cached client return it instead + IF is_session_running( ) = abap_true AND mi_authenticated_session IS BOUND. + ri_client = mi_authenticated_session. RETURN. ENDIF. @@ -586,8 +635,14 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. ENDIF. " Cache the authenticated http session / client to avoid unnecessary additional authentication - ss_cached_client-username = iv_username. - ss_cached_client-client = ri_client. + IF is_session_running( ) = abap_true. + mi_authenticated_session = ri_client. + ENDIF. + ENDMETHOD. + + METHOD end. + super->end( ). + FREE mi_authenticated_session. ENDMETHOD. ENDCLASS. @@ -683,6 +738,7 @@ CLASS lcl_2fa_authenticator_registry IMPLEMENTATION. TRY. li_authenticator = get_authenticator_for_url( iv_url ). + li_authenticator->begin( ). " Is two factor authentication required for this account? IF li_authenticator->is_2fa_required( iv_url = iv_url @@ -716,7 +772,14 @@ CLASS lcl_2fa_authenticator_registry IMPLEMENTATION. cv_password = lv_access_token. ENDIF. + li_authenticator->end( ). + CATCH lcx_2fa_error INTO lx_ex. + TRY. + li_authenticator->end( ). + CATCH lcx_2fa_illegal_state ##NO_HANDLER. + ENDTRY. + RAISE EXCEPTION TYPE lcx_exception EXPORTING iv_text = |2FA error: { lx_ex->get_text( ) }| From e3165461983ae36d3b94b470dda64d3cfc08f91f Mon Sep 17 00:00:00 2001 From: Fabian Lupa Date: Sun, 5 Feb 2017 16:41:49 +0100 Subject: [PATCH 14/14] Wait 1 second after access token generation --- src/zabapgit_2fa.prog.abap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap index 329b44a4d..5a2883313 100644 --- a/src/zabapgit_2fa.prog.abap +++ b/src/zabapgit_2fa.prog.abap @@ -373,6 +373,9 @@ CLASS lcl_2fa_github_authenticator IMPLEMENTATION. EXPORTING iv_error_text = 'Token generation failed: parser error' ##NO_TEXT. ENDIF. + + " GitHub might need some time until the new token is ready to use, give it a second + CALL FUNCTION 'RZL_SLEEP'. ENDMETHOD. METHOD set_new_token_request.