Пришло время когда интеграции со сторонними организациями и их приложениями стало необходимостью для успешного ведения бизнеса. В этот статье будет рассмотрено прохождение авторизации 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
ШАГ 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
, таблица имеет вид:
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.
Вместо послесловия, полноценную рабочую версию не выкладываю из-за ограниченной лицензии, но мне кажется, не составит большого труда дописать недостающие части. Успехов в начинаниях.
Петр, добрый день! Очень полезная статья, спасибо!
если у Вас будет минутка, как с Вами можно связаться? Стоит задача по интеграции с QuickBooks Online (с десктопной версией интеграция есть, но новые клиенты приходят уже с онлайн). Есть несколько общих вопросов. Был бы очень благодарен.
Спасибо
Доброго времени суток, если у вас есть предложения и вопросы по поводу сотрудничества можете написать мне на почту pbazeliuk@gmail.com.