Как сделать конфигурацию «1С:Предприятие 8» приложением QuickBooks. Проходим авторизацию OAuth 1.0a+OpenID 2.0

Программирование - Практика программирования

Пришло время, когда интеграция со сторонними организациями и их приложениями стала необходимостью для успешного ведения бизнеса. В этой статье будет рассмотрено прохождение авторизации OAuth 1.0a+OpenID 2.0 и превращение конфигурации «1С:Предприятие 8» в приложение QuickBooks.

Вводные данные:

Прежде чем переходить к основной теме статьи, необходимо сделать лирическое отступление и описать некоторые неизвестные слова для неискушенного разработчика.

OAuth 1.0a — это открытый протокол авторизации, который позволяет предоставить третьей стороне ограниченный доступ к защищённым ресурсам пользователя без необходимости передавать ей (третьей стороне) логин и пароль. С детальным описанием протокола можно ознакомится в информационном документе RFC 5849.

OpenID 2.0 — открытый стандарт децентрализованной системы аутентификации, предоставляющей пользователю возможность создать единую учётную запись для аутентификации на множестве не связанных друг с другом интернет-ресурсов, используя услуги третьих лиц.

QuickBooks — представляет собой пакет программного обеспечения для бухгалтерского учета. Продукты QuickBooks ориентированы главным образом на малые и средние предприятия и предлагают приложения для бухгалтерского учета на местах, а также версии на основе облачных вычислений.

Проблематика:

Многие компании за границей используют QuickBooks для бухгалтерского учета и все конфигурации «1С:Предприятие 8», которые фирма 1С предоставляет через известный сайт для западного рынка, пока не могут закрыть вопросы бухгалтерского учета. В то же время конфигурации подходят для складского учета и для других целей. На этом стыке возникает потребность в интеграции систем. Большинство программистов привыкли, что интеграция — это обмен сообщениями определенного формата, но когда они встречают столь сложную авторизацию и аутентификацию невольно опускаются руки.

QuickBooks использует комбинацию OAuth 1.0a + OpenID 2.0 для интеграции через приложение (App). Приложение можно создать по ссылке. После создания приложения станут доступны ключи доступа. Вот об комбинировании этой солянки и пойдет речь ниже.

ШАГ 1. Создаем приложение:

После создания приложения, необходимо получить ключи разработчика для песочницы. В нашем случае они будут:
oauth_consumer_key = qyprdEQRuexcfM4P05zd2vB0baw2TB 
oauth_consumer_secret = AbQpDjBAQlTHt08GBT9yktfuklrZJkvBL7Hswhoc
Ключи разработчика для песочницы QuickBooks

ШАГ 2. Получаем request token

Приложение должно запросить набор временных учетных данных token, также известный как request token. Он еще не связаны с какой-либо компанией QuickBooks конкретного пользователя. Приложением будет выступать конфигурация «1С:Предприятие 8» и для прохождения данного этапа необходимо реализовать некоторую базовую функциональность:

// Returns unique token your application should generate for each unique request.
//
// Returns:
//  String.
//
Function OAuthNonce()
    Return String(New UUID);  
EndFunction // OAuthNonce()

// Returns default oauth signature method name.
//
// Returns:
//  String.
//
Function OAuthSignatureMethod()
    Return "HMAC-SHA1";
EndFunction // OAuthSignatureMethod()

// Returns a sequence of characters or encoded information identifying when a certain 
// event occurred, usually giving date and time of day, sometimes accurate to a small 
// fraction of a second.
//
// Returns:
//  String.
//
Function OAuthTimestamp()
    Return Format(CurrentSessionDate() - Date("19700101"), "NG=0");
EndFunction // OAuthTimestamp()

// Returns the OAuth version 1.0.
//
// Returns:
//  String.
//
Function OAuthVersion()
    Return "1.0";
EndFunction // OAuthVersion()

Так же реализуем в виде функций ссылки (URL) для прохождения авторизации:

// Only for internal use.
//
Function AuthorizeUrl() Export 
    Return "https://appcenter.intuit.com/Connect/Begin";   
EndFunction // AuthorizeUrl()

// Only for internal use.
//
Function CallbackUrl() Export
    Return "https://httpbin.org/forms/post?";  
EndFunction // CallbackUrl()

// Only for internal use.
//
Function TokenServerName()
    Return "https://oauth.intuit.com";   
EndFunction // TokenServerName()

// Only for internal use.
//
Function RequestTokenResource()
    Return "/oauth/v1/get_request_token";   
EndFunction // RequestTokenResource()

// Only for internal use.
//
Function AccessTokenResource()
    Return "/oauth/v1/get_access_token";   
EndFunction // AccessTokenResource()

// Only for internal use.
//
Function AppServerName() 
    Return "https://appcenter.intuit.com";
EndFunction // AppServerName()

// Only for internal use.
//
Function AppDisconnectResource()
    Return "/api/v1/connection/disconnect";
EndFunction // AppDisconnectResource()

Следующий код реализует получение `request token`:

// Only for internal use.
//
Procedure RequestToken() Export
        
    OAuthList = New ValueList();
    OAuthList.Add(OAuthNonce(),           "oauth_nonce");
    OAuthList.Add(OAuthVersion(),         "oauth_version");
    OAuthList.Add(CallbackUrl(),          "oauth_callback");
    OAuthList.Add(OAuthTimestamp(),       "oauth_timestamp");
    OAuthList.Add(OAuthConsumerKey(),     "oauth_consumer_key");
    OAuthList.Add(OAuthSignatureMethod(), "oauth_signature_method");
        
    Parameters = NewHTTPSecureParameters();
    Parameters.HTTPMethod = "GET";
    Parameters.ServerName = TokenServerName();
    Parameters.Resource   = RequestTokenResource();
    Parameters.Headers.Insert("Authorization", 
        AuthorizationHeader(Parameters.HTTPMethod, 
            Parameters.ServerName, 
            Parameters.Resource, 
            OAuthList));
    Parameters.Fields.Add("oauth_token");
    Parameters.Fields.Add("oauth_token_secret");
        
    HTTPSecureRequest(Parameters, EncryptedData);
    
EndProcedure // RequestToken()

Функция NewHTTPSecureParameters описывает структуру параметров запроса.
Методы шифрования в данной статье не рассматриваются и потому, EncryptFields не будет использован.

// Only for internal use.
//
Function NewHTTPSecureParameters() Export
    
    Parameters = New Structure;
    Parameters.Insert("HTTPMethod");
    Parameters.Insert("ServerName");
    Parameters.Insert("Resource");
    Parameters.Insert("Headers", New Map);
    Parameters.Insert("Fields", New Array);
    Parameters.Insert("EncryptFields", New Array);    
    Return Parameters;
    
EndFunction // NewHTTPSecureParameters()

Функция AuthorizationHeader формирует заголовок авторизации.

// Only for internal use.
//
Function AuthorizationHeader(HTTPMEthod, ServerName, Resource, OAuthList)
    
    RequestUrl = StrTemplate("%1%2", ServerName, Resource);
    
    Signature = "";
    URLEncoding = StringEncodingMethod.URLEncoding;
    
    URIStructure = URIStructure(RequestUrl);
    For Each Parameter In URIStructure.Parameters Do
        OAuthList.Add(Parameter.Value, Parameter.Key);   
    EndDo;
    
    OAuthList.SortByPresentation();
    For Each OAuthItem In OAuthList Do
        
        If IsBlankString(Signature) Then
            Signature = Signature + OAuthItem.Presentation + "=" 
                + EncodeString(OAuthItem.Value, URLEncoding);    
        Else
            Signature = Signature + "&" + OAuthItem.Presentation + "=" 
                + EncodeString(OAuthItem.Value, URLEncoding);   
        EndIf;
        
    EndDo;
    
    Position = StrFind(RequestUrl, "?");
    If Position > 0 Then
        RequestUrl = Left(RequestUrl, Position - 1);    
    EndIf;
    
    OAuthList.Add(OAuthSignature(HTTPMEthod, RequestUrl, 
        OAuthSignatureMethod(), Signature, EncryptedData), "oauth_signature");

            
    Authorization = "OAuth ";
    For Each OAuthItem In OAuthList Do
        
        If StrFind(OAuthItem.Presentation, "oauth_") = 1 Then
            
            If Authorization = "OAuth " Then
                Authorization = Authorization + OAuthItem.Presentation + "=""" 
                    + EncodeString(OAuthItem.Value, URLEncoding) + """";
            Else 
                Authorization = Authorization + "," + OAuthItem.Presentation + "=""" 
                    + EncodeString(OAuthItem.Value, URLEncoding) + """";
            EndIf;
                
        EndIf;
        
    EndDo;
             
    Return Authorization;    
    
EndFunction // AuthorizationHeader()

Функция URIStructure предназначена для разбора ссылки на составляющие.

// Dissembles URI string and returns it as a structure.
// Based on RFC 3986.
//
// Parameters:
//  StringURI - String - reference to the resource in the format:
//      <schema>://<login>:<password>@<host>:<port>/<path>?<parameters>#<anchor>
//
// Returns:
//  Structure - with keys:
//      * Schema       - String - schema.
//      * Login        - String - user login.
//      * Password     - String - user password.
//      * ServerName   - String - part : from the StringURI.
//      * Host         - String - host name.
//      * Port         - Number - port number.
//      * PathOnServer - String - part ?# from the StringURI.
//      * Parameters   - Map    - parsed parameters from the StringURI. 
//
Function URIStructure(Val StringURI) Export

    StringURI = TrimAll(StringURI);
    Parameters = New Map;
    
    // Schema
    Schema = "";
    Position = StrFind(StringURI, "://");
    If Position > 0 Then
        Schema = Lower(Left(StringURI, Position - 1));
        StringURI = Mid(StringURI, Position + 3);
    EndIf;

    // Connection string and path on the server.
    ConnectionString = StringURI;
    PathOnServer = "";
    Position = StrFind(ConnectionString, "/");
    If Position > 0 Then
        PathOnServer = Mid(ConnectionString, Position + 1);
        ConnectionString = Left(ConnectionString, Position - 1);
    EndIf;
    
    // Parameters
    Position = StrFind(PathOnServer, "?");
    If Position > 0 Then
        ParametersString = Mid(PathOnServer, Position + 1);        
        ParametersArray = StrSplit(ParametersString, "&");
        For Each Parameter In ParametersArray Do
            Position = StrFind(Parameter, "=");
            If Position > 1 Then
                Parameters.Insert(Left(Parameter, Position - 1), Mid(Parameter, Position + 1));    
            EndIf;    
        EndDo;
    EndIf;
        
    // User information and server name.
    AuthorizeString = "";
    ServerName = ConnectionString;
    Position = StrFind(ConnectionString, "@");
    If Position > 0 Then
        AuthorizeString = Left(ConnectionString, Position - 1);
        ServerName = Mid(ConnectionString, Position + 1);
    EndIf;

    // Login and password.
    Login = AuthorizeString;
    Password = "";
    Position = StrFind(AuthorizeString, ":");
    If Position > 0 Then
        Login = Left(AuthorizeString, Position - 1);
        Password = Mid(AuthorizeString, Position + 1);
    EndIf;

    // Host and port.
    Host = ServerName;
    Port = "";
    Position = StrFind(ServerName, ":");
    If Position > 0 Then
        
        Host = Left(ServerName, Position - 1);
        Port = Mid(ServerName, Position + 1); 
        For Index = 1 To StrLen(Port) Do
            Symbol = Mid(Port, Index, 1);
            If Not IsNumber(Symbol) Then
                Port = "";
                Break;    
            EndIf;
            
        EndDo;
        
        If IsBlankString(Port) Then
            If Schema = "http" Then
                Port = "80";
            ElsIf Schema = "https" Then
                Port = "443";
            EndIf;
        EndIf;
 
    EndIf;

    Result = New Structure;
    Result.Insert("Schema", Schema);
    Result.Insert("Login", Login);
    Result.Insert("Password", Password);
    Result.Insert("ServerName", ServerName);
    Result.Insert("Host", Host);
    Result.Insert("Port", ?(IsBlankString(Port), Undefined, Number(Port)));
    Result.Insert("PathOnServer", PathOnServer);
    Result.Insert("Parameters", Parameters);

    Return Result;

EndFunction // URIStructure()

Функция OAuthSignature формирует подпись из переданных данных.
Внимание на реализации функции HMAC_SHA1(UrlSignature, KeySignature) заострять не буду оно довольно тривиальное и достаточно легко реализуется.
Методы шифрования в данной статье не рассматриваются и потому ветка IsBlankString(EncryptNumber) не имеет реализации чтения зашифрованных значений.

Function OAuthSignature(Val HTTPMethod, Val URL, Val OAuthSignatureMethod, 
    Val PreparedSignature, EncryptedData) Export
    
    UrlSignature = HTTPMethod + "&" 
        + EncodeString(URL, StringEncodingMethod.URLEncoding) + "&" 
        + EncodeString(PreparedSignature, StringEncodingMethod.URLEncoding);
   
    KeySignature = EncryptedFiledValue("oauth_consumer_secret", EncryptedData) 
        + "&" + EncryptedFiledValue("oauth_token_secret", EncryptedData);
    
    If Upper(TrimAll(OAuthSignatureMethod)) = "HMAC-SHA1" Then
 
        Signature = HMAC_SHA1(UrlSignature, KeySignature);
        
    Else
        
        ErrorMessage = NStr("en = 'Signature method is not supported.';
            |ru = 'Сигнатурный метод не поддерживаеться.'");
            
        Raise ErrorMessage;
        
    EndIf;
    
    Return Signature;           
    
EndFunction // OAuthSignature()

// Only for internal use.
//
Function EncryptedFiledValue(FieldName, EncryptedData)
    
    SearchResult = EncryptedData.Find(FieldName, "FieldName");
    If SearchResult <> Undefined Then
        
        FieldValue = SearchResult.FieldValue;
        EncryptNumber = SearchResult.EncryptNumber;
        If IsBlankString(EncryptNumber) Then
            Return FieldValue;
        Else
            Return "";
        EndIf;
        
    Else
        
        Return "";    
        
    EndIf;
  
EndFunction // EncryptedFiledValue()

Процедура HTTPSecureRequest, непосредственно, выполняет обращение к серверу авторизации и при успешном результате полученные данные добавляет в таблицу EncryptedData, таблица имеет вид:
Реквизиты таблицы с зашифрованными данными OAuth 1.0a

Procedure HTTPSecureRequest(Parameters, EncryptedData) Export
    
    URIStructure = IHL_CommonUseClientServer.URIStructure(
        Parameters.ServerName + Parameters.Resource);
    
    HTTPRequest = New HTTPRequest(URIStructure.PathOnServer);
    For Each Header In Parameters.Headers Do 
        HTTPRequest.Headers.Insert(Header.Key, Header.Value);
    EndDo;
    
    HTTPConnection = New HTTPConnection(
        URIStructure.Host,
        URIStructure.Port,
        URIStructure.Login,
        URIStructure.Password,
        ,
        ,
        New OpenSSLSecureConnection(Undefined, Undefined));
        
    HTTPResponse = HTTPConnection.CallHTTPMethod(Parameters.HTTPMethod, 
        HTTPRequest);
    
    If HTTPResponse.StatusCode = 200 Then
        
        ContentType = HTTPResponse.Headers.Get("Content-Type");
        If ContentType = "text/plain" Then
            
            Body = HTTPResponse.GetBodyAsString();
            BodyParts = StrSplit(Body, "&");
            For Each BodyPart In BodyParts Do
                
                Position = StrFind(BodyPart, "=");
                If Position > 0 Then
                    FieldName  = Left(BodyPart, Position - 1);
                    FieldValue = Mid(BodyPart, Position + 1);
                    If Parameters.Fields.Find(FieldName) <> Undefined Then
                        RowResult = EncryptedData.Find(FieldName, "FieldName");
                        If RowResult = Undefined Then
                            RowResult = EncryptedData.Add();
                        EndIf;
                        RowResult.FieldName  = FieldName;
                        RowResult.FieldValue = FieldValue;
                    EndIf;
                EndIf;
            EndDo;
        EndIf;
    Else
        Raise HTTPResponse.GetBodyAsString();
    EndIf;
    
EndProcedure // HTTPSecureRequest()

ШАГ 3. Авторизация пользователя и приложения на QuickBooks

На этом шаге конфигурация «1С:Предприятие 8» должна перенаправить пользователя на ресурс для авторизации на адрес https://appcenter.intuit.com/Connect/Begin?oauth_token=token, где oauth_token равен полученному на предыдущем этапе. После успешной авторизации пользователя и предоставления доступа приложения к компании в QuickBooks, будет выполнено переход на CallbackUrl (вы можете разместить сервер который будет обрабатывать запросы или воспользоваться перехватом управления как выполнено в видео, что ниже).

ШАГ 4. Замена временного токена на постоянный

На последнем шаге необходимо заменить временный oauth_token на постоянный:

  • Функция OAuthToken() возвращает временный token полученный на шаге 2;
  • Функция OAuthVerifier() возвращает значение oauth_verifier, которое было получено на шаге 3 при переадресации на CallbackUrl:
// Only for internal use.
//
Procedure AccessToken() Export
    
    OAuthList = New ValueList();
    OAuthList.Add(OAuthNonce(),             "oauth_nonce");
    OAuthList.Add(OAuthToken(),             "oauth_token");
    OAuthList.Add(OAuthVersion(),           "oauth_version");
    OAuthList.Add(OAuthVerifier(),          "oauth_verifier");
    OAuthList.Add(OAuthTimestamp(),         "oauth_timestamp");
    OAuthList.Add(OAuthConsumerKey(),       "oauth_consumer_key");
    OAuthList.Add(OAuthSignatureMethod(),   "oauth_signature_method");
    
    Parameters = NewHTTPSecureParameters();
    Parameters.HTTPMethod = "GET";
    Parameters.ServerName = TokenServerName();
    Parameters.Resource   = AccessTokenResource();
    Parameters.Headers.Insert("Authorization", 
        AuthorizationHeader(Parameters.HTTPMethod, 
            Parameters.ServerName,
            Parameters.Resource,
            OAuthList));
    Parameters.Fields.Add("oauth_token");
    Parameters.Fields.Add("oauth_token_secret");
        
    HTTPSecureRequest(Parameters, EncryptedData);
      
EndProcedure // AccessToken()

После успешного выполнения необходимо сохранить данные таблицы EncryptedData и в дальнейшем использовать их для формирования AuthorizationHeader для выполнения запросов к ресурсам компании QuickBooks.

Вместо послесловия, полноценную рабочую версию не выкладываю из-за ограниченной лицензии, но мне кажется, не составит большого труда дописать недостающие части. Успехов в начинаниях.

Статья в личном блоге клац

См. также

Комментарии
1. Вова Вишин (Tahallus) 421 11.09.17 03:43 Сейчас в теме
Если не секрет можете рассказать для чего и как используется QuickBooks + 1С
2. Петр Базелюк (pbazeliuk) 1364 11.09.17 07:35 Сейчас в теме
(1) QuickBooks полноценная бухгалтерская система, в свою очередь конфигурация 1С используется как система для складского учета (1C:Small Business). Во многих организациях есть подобные связки бухгалтерия + складская или управленческая система.
3. Ruslan Ruslan (flyer) 246 11.09.17 12:39 Сейчас в теме
(2) у QuickBooks разве нет модуля управление складом?
4. Петр Базелюк (pbazeliuk) 1364 11.09.17 13:24 Сейчас в теме
(3) В "1С:Бухгалтерии" так же есть управление складом, но, как-то, не очень удобно, удобней "1С:Управление торговлей"... В "1С:Бухгалтерии" так же есть возможность учитывать розничные продажи, но, как-то, не очень удобно, удобней "1С:Розница"... и таких примеров масса.

В QuickBooks можно взять в оренду WMS как приложение, тут уже вопрос в цене.
5. kolya_tlt kolya_tlt (kolya_tlt) 11 12.09.17 14:20 Сейчас в теме
(4) если честно, так до конца и не понятно зачем делать какую-то конфигурацию.
у меня вот какой вопрос: удавалось ли решать вопросы единой аутентификации для всех систем, как в примере выше? QuickBooks + 1С.
затык заключается, в том, что openID 1C можно аутентифицироваться только в системах 1С, а хотелось бы иметь доступ во все платформы.
6. Петр Базелюк (pbazeliuk) 1364 12.09.17 14:37 Сейчас в теме
(5) если говорить об OpenID, то у стандарта всегда есть 3 стороны, в данном случае QuickBooks выступает как провайдер, клиент как клиент зарегистрированный у провайдера, 1С конфигурация как приложение, которое получает доступ к данным.
Для других систем так же можно пройти авторизацию, если провайдер поддерживается этими системами.

Аутентификацию к различным системам есть возможность выполнить с помощью https://infostart.ru/public/560516/ версии выше чем 0.9.0 (которая пока не доступна, но готовится к релизу, а пока делается описание, шаблоны для того чтобы выложить встраиваемую конфигурацию на общее обозрение).
7. kolya_tlt kolya_tlt (kolya_tlt) 11 12.09.17 14:54 Сейчас в теме
(6) у нас потребность не получении данных, в том, чтобы выполнив вход в провайдере (который не на 1С) попасть в систему 1С как в приложение.
8. Петр Базелюк (pbazeliuk) 1364 12.09.17 15:16 Сейчас в теме
(7) То что вы спрашиваете это не тема этой статьи, совсем. Но возможность такое сделать есть. Подали мне идею для платного модуля :)
9. kolya_tlt kolya_tlt (kolya_tlt) 11 12.09.17 16:35 Сейчас в теме
10. Natalya Kovalenko (natalic) 18.09.17 09:38 Сейчас в теме
Здравствуйте! в какой программе реализуется весь функционал? где писать код?
11. Петр Базелюк (pbazeliuk) 1364 18.09.17 10:30 Сейчас в теме
(10) Добрый день, все реализуется в «1С:Предприятие 8», после прохождения авторизации можно использовать все команды из API QuickBooks.
12. Биг Босс (BigBoss) 3 15.11.17 09:29 Сейчас в теме
(11) День добрый, вопрос: я как понимаю интеграция заключается в следующем: склад и всё что с ним связанно оформляется в 1С, а фин.операции оформляются в QuickBooks?
13. Петр Базелюк (pbazeliuk) 1364 15.11.17 10:16 Сейчас в теме
(12) Все зависит от того, какая конфигурация выбрана за основу и на сколько тесной необходима интеграция.
Оставьте свое сообщение