Как сделать конфигурацию «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.

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

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

28 Comments

  1. Tahallus

    Если не секрет можете рассказать для чего и как используется QuickBooks + 1С

    Reply
  2. pbazeliuk

    (1) QuickBooks полноценная бухгалтерская система, в свою очередь конфигурация 1С используется как система для складского учета (1C:Small Business). Во многих организациях есть подобные связки бухгалтерия + складская или управленческая система.

    Reply
  3. flyer

    (2) у QuickBooks разве нет модуля управление складом?

    Reply
  4. pbazeliuk

    (3) В «1С:Бухгалтерии» так же есть управление складом, но, как-то, не очень удобно, удобней «1С:Управление торговлей»… В «1С:Бухгалтерии» так же есть возможность учитывать розничные продажи, но, как-то, не очень удобно, удобней «1С:Розница»… и таких примеров масса.

    В QuickBooks можно взять в оренду WMS как приложение, тут уже вопрос в цене.

    Reply
  5. kolya_tlt

    (4) если честно, так до конца и не понятно зачем делать какую-то конфигурацию.

    у меня вот какой вопрос: удавалось ли решать вопросы единой аутентификации для всех систем, как в примере выше? QuickBooks + 1С.

    затык заключается, в том, что openID 1C можно аутентифицироваться только в системах 1С, а хотелось бы иметь доступ во все платформы.

    Reply
  6. pbazeliuk

    (5) если говорить об OpenID, то у стандарта всегда есть 3 стороны, в данном случае QuickBooks выступает как провайдер, клиент как клиент зарегистрированный у провайдера, 1С конфигурация как приложение, которое получает доступ к данным.

    Для других систем так же можно пройти авторизацию, если провайдер поддерживается этими системами.

    Аутентификацию к различным системам есть возможность выполнить с помощью https://infostart.ru/public/560516/ версии выше чем 0.9.0 (которая пока не доступна, но готовится к релизу, а пока делается описание, шаблоны для того чтобы выложить встраиваемую конфигурацию на общее обозрение).

    Reply
  7. kolya_tlt

    (6) у нас потребность не получении данных, в том, чтобы выполнив вход в провайдере (который не на 1С) попасть в систему 1С как в приложение.

    Reply
  8. pbazeliuk

    (7) То что вы спрашиваете это не тема этой статьи, совсем. Но возможность такое сделать есть. Подали мне идею для платного модуля 🙂

    Reply
  9. kolya_tlt

    (8) с вас 40% 🙂

    Reply
  10. natalic

    Здравствуйте! в какой программе реализуется весь функционал? где писать код?

    Reply
  11. pbazeliuk

    (10) Добрый день, все реализуется в «1С:Предприятие 8», после прохождения авторизации можно использовать все команды из API QuickBooks.

    Reply
  12. BigBoss

    (11) День добрый, вопрос: я как понимаю интеграция заключается в следующем: склад и всё что с ним связанно оформляется в 1С, а фин.операции оформляются в QuickBooks?

    Reply
  13. pbazeliuk

    (12) Все зависит от того, какая конфигурация выбрана за основу и на сколько тесной необходима интеграция.

    Reply
  14. ylyas

    Ни где не нашел листинг процедуры из общего модуля: IHL_CommonUseClientServer.URIStructure

    Можете привести?

    Reply
  15. pbazeliuk
  16. ylyas

    (15) Благодарю, очень любезно с вашей стороны. Очень полезный экспириенс.

    Reply
  17. 0SpAGeTTi010

    Добрый Вечер! Опишите сам процесс подключения к песочнице.

    Не совсем понятно, как ориентироваться на сайте, и какие поля там понадобятся в подключении. Спасибо

    Reply
  18. pbazeliuk

    (17) About 18 months ago, we announced support for OAuth 2.0. At that time, all new apps started using OAuth 2.0 for authorization, and (optionally) OpenID Connect for single sign-on authentication.

    We also launched an open OAuth 2.0 migration beta program so developers could give us feedback on our tools and resources. Over the past year, we have provided several OAuth 2.0 libraries, and included modules to migrate OAuth 1.0 tokens to OAuth 2.0 including, most recently, when we released libraries in Python and Node.js.

    Если кратко, статья уже не актуальна. Уже OAuth 2.0, там все проще и проблем не должно возникать

    Reply
  19. ylyas

    (18)

    Боюсь, что вы не до оцениваете ценность вашей статьи.

    Мне, например, не к QuickBooks подключаться нужно.

    И метод сигнатур другой используется.

    Но есть еще куча ресурсов, которые по прежнему пользуют oauth1.0a( как в моем случае).

    По этом ваша публикация очень хороший и наглядный пример для меня. 🙂

    Reply
  20. ylyas

    У меня вот к вам другой вопрос, Петр.

    скажите, порядок выполнения запросов для авторизации(request token ->access token) всегда остается один и тот же независимо от oauth_signature_method ?

    Просто в моем случае это private app и RSA-SHA1. То есть, на сайт выложен public key а у меня есть private key.

    Или в таком случае уже не нужно запрашивать токен доступа? можно сразу выполнять запрос к данным передавая секретный ключ(oauth_token_secret) в заголовке?

    Reply
  21. 0SpAGeTTi010

    Конечно, подключение к QuickBooks oline большая проблема для меня, и эта статься единственное от чего я отталкивался за все это время, правда все безрезультатно.

    Теперь, у меня возник вопрос к подключению QuickBooks Desktop, вот тут на самом деле все проще должно быть, но нигде нет примеров подключения на встроенном языке 1С.

    qbsdk130.exe я уже установил, хочу знать как запускать его программно из модуля 1с

    Reply
  22. ylyas

    (21) пробуйте ЗапуститьПриложение

    Reply
  23. pbazeliuk

    (20) Пришлите ссылку на API, я вам порядок в личку распишу.

    Reply
  24. 0SpAGeTTi010

    (22)Добрый день и С новым годом! Спасибо за предложенный вариант, но у меня нет необходимости открывать демонстративно qbsdk130.exe в 1С.

    Мне нужно пользоваться инструменты qbsdk130.exe для интеграции. Как подключаться к qbsdk130.exe из модуля 1С, мне не известно, в интернете нет ни каких примеров(

    Reply
  25. ylyas

    (24)

    Не понимаю о чем речь. вообще. пишите в личку. если интересно

    Reply
  26. 0SpAGeTTi010

    (25)Да, интересно. Напишите мне первым, у меня нет возможности до Вас дописаться

    Reply
  27. 0SpAGeTTi010

    Добрый день! Вы у меня в друзьях, а написать Вам не могу. Напишите мне первым, пожалуйста. У меня один вопросик будет

    Reply
  28. minlebay

    (27) Добрый день! Получилось ли у Вас подключиться к QuickBooks Online?

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *