diff --git a/src/libcmis/oauth2-providers.cxx b/src/libcmis/oauth2-providers.cxx --- a/src/libcmis/oauth2-providers.cxx +++ b/src/libcmis/oauth2-providers.cxx @@ -29,9 +29,15 @@ #include #include +#include "session-factory.hxx" #include "oauth2-providers.hxx" #include "http-session.hxx" +#define CHALLENGE_PAGE_ACTION "/signin" +#define CHALLENGE_PAGE_ACTION_LEN sizeof( CHALLENGE_PAGE_ACTION ) - 1 +#define PIN_FORM_ACTION "/signin/challenge/ipp" +#define PIN_FORM_ACTION_LEN sizeof( PIN_FORM_ACTION ) - 1 + using namespace std; #if LIBXML_VERSION < 20621 @@ -51,10 +52,23 @@ string OAuth2Providers::OAuth2Gdrive( HttpSession* session, const string& authUr * 4) subsequent post to send a consent for the application * receive a single-use authorization code * this code is returned as a string + * + * Sequence with 2FA is: + * 1) a get to activate login page + * receive first login page, html format + * 2) subsequent post to sent email + * receive html page for password input + * 3) subsequent post to send password + * receive html page for pin input + * 3b) subsequent post to send pin number + * receive html page for application consent + * 4) subsequent post to send a consent for the application + * receive a single-use authorization code + * this code is returned as a string */ static const string CONTENT_TYPE( "application/x-www-form-urlencoded" ); - // STEP 1: Log in + // STEP 1: get login page string res; try { @@ -66,6 +80,8 @@ string OAuth2Providers::OAuth2Gdrive( HttpSession* session, const string& authUr return string( ); } + // STEP 2: send email + string loginEmailPost, loginEmailLink; if ( !parseResponse( res.c_str( ), loginEmailPost, loginEmailLink ) ) return string( ); @@ -86,6 +102,8 @@ string OAuth2Providers::OAuth2Gdrive( HttpSession* session, const string& authUr return string( ); } + // STEP 3: password page + string loginPasswdPost, loginPasswdLink; if ( !parseResponse( loginEmailRes.c_str( ), loginPasswdPost, loginPasswdLink ) ) return string( ); @@ -106,10 +124,60 @@ string OAuth2Providers::OAuth2Gdrive( HttpSession* session, const string& authUr return string( ); } - // STEP 2: allow libcmis to access google drive string approvalPost, approvalLink; if ( !parseResponse( loginPasswdRes. c_str( ), approvalPost, approvalLink) ) return string( ); + + // when 2FA is enabled, link doesn't start with 'http' + if ( approvalLink.compare(0, 4, "http") != 0 ) + { + // STEP 3b: 2 Factor Authentication, pin code request + + string loginChallengePost( approvalPost ); + string loginChallengeLink( approvalLink ); + + libcmis::OAuth2AuthCodeProvider fallbackProvider = libcmis::SessionFactory::getOAuth2AuthCodeProvider( ); + string pin( fallbackProvider( "", "", "" ) ); + + if( pin.empty() ) + { + // unset OAuth2AuthCode Provider to avoid showing pin request again in the HttpSession::oauth2Authenticate + libcmis::SessionFactory::setOAuth2AuthCodeProvider( NULL ); + return string( ); + } + + loginChallengeLink = "https://accounts.google.com" + loginChallengeLink; + loginChallengePost += "Pin="; + loginChallengePost += string( pin ); + + istringstream loginChallengeIs( loginChallengePost ); + string loginChallengeRes; + try + { + // send a post with pin, receive the application consent page + loginChallengeRes = session->httpPostRequest ( loginChallengeLink, loginChallengeIs, CONTENT_TYPE ) + ->getStream( )->str( ); + } + catch ( const CurlException& e ) + { + return string( ); + } + + approvalPost = string(); + approvalLink = string(); + + if ( !parseResponse( loginChallengeRes. c_str( ), approvalPost, approvalLink) ) + return string( ); + } + else if( approvalLink.compare( "https://accounts.google.com/ServiceLoginAuth" ) == 0 ) + { + // wrong password, + // unset OAuth2AuthCode Provider to avoid showing pin request again in the HttpSession::oauth2Authenticate + libcmis::SessionFactory::setOAuth2AuthCodeProvider( NULL ); + return string( ); + } + + // STEP 4: allow libcmis to access google drive approvalPost += "submit_access=true"; istringstream approvalIs( approvalPost ); @@ -125,7 +186,7 @@ string OAuth2Providers::OAuth2Gdrive( HttpSession* session, const string& authUr throw e.getCmisException( ); } - // STEP 3: Take the authentication code from the text bar + // Take the authentication code from the text bar string code = parseCode( approvalRes.c_str( ) ); return code; @@ -216,6 +277,9 @@ int OAuth2Providers::parseResponse ( const char* response, string& post, string& if ( doc == NULL ) return 0; xmlTextReaderPtr reader = xmlReaderWalker( doc ); if ( reader == NULL ) return 0; + + bool readInputField = false; + while ( true ) { // Go to the next node, quit if not found @@ -227,15 +291,30 @@ int OAuth2Providers::parseResponse ( const char* response, string& post, string& { xmlChar* action = xmlTextReaderGetAttribute( reader, BAD_CAST( "action" )); + + // GDrive pin code page contains many forms. + // We have to parse only the form with pin field. if ( action != NULL ) { - if ( xmlStrlen(action) > 0) + bool bChallengePage = ( strncmp( (char*)action, + CHALLENGE_PAGE_ACTION, + CHALLENGE_PAGE_ACTION_LEN ) == 0 ); + bool bIsRightForm = ( strncmp( (char*)action, + PIN_FORM_ACTION, + PIN_FORM_ACTION_LEN ) == 0 ); + if ( ( xmlStrlen( action ) > 0 ) + && ( ( bChallengePage && bIsRightForm ) || !bChallengePage ) ) + { link = string ( (char*) action); + readInputField = true; + } + else + readInputField = false; xmlFree (action); } } // Find input values - if ( !xmlStrcmp( nodeName, BAD_CAST( "input" ) ) ) + if ( readInputField && !xmlStrcmp( nodeName, BAD_CAST( "input" ) ) ) { xmlChar* name = xmlTextReaderGetAttribute( reader, BAD_CAST( "name" ));