Skip to content

Feature/sslpinning #24

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ Naming/AccessorMethodName:
# ( ) for method calls
Style/MethodCallWithArgsParentheses:
Enabled: true
Exclude:
- "**/*.podspec"
IgnoredMethods:
- 'require'
- 'require_relative'
Expand Down
24 changes: 17 additions & 7 deletions Example/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {

let factory = TinkoffIDFactory(
clientId: clientId,
callbackUrl: callbackUrl
callbackUrl: callbackUrl,
webViewSourceProvider: self
)

return factory.build()
}()

lazy var authController: AuthViewController = {
AuthViewController(signInInitializer: tinkoffId,
credentialsRefresher: tinkoffId,
signOutInitializer: tinkoffId)
}()

func applicationDidFinishLaunching(_ application: UIApplication) {
let authController = AuthViewController(
signInInitializer: tinkoffId,
credentialsRefresher: tinkoffId,
signOutInitializer: tinkoffId
)
authController.tabBarItem = UITabBarItem(title: "Auth", image: nil, tag: 0)

let tinkoffButtonsController = TinkoffButtonsViewController()
Expand All @@ -65,6 +67,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
tinkoffId.handleCallbackUrl(url)
return tinkoffId.handleCallbackUrl(url)
}
}

// MARK: - AuthWebView Usage

extension AppDelegate: IAuthWebViewSourceProvider {
func getSourceViewController() -> UIViewController {
authController
}
}
29 changes: 29 additions & 0 deletions Example/App/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,34 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>certs.tinkoff.ru</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>tcsbank.ru</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>tinkoff.ru</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>
2 changes: 2 additions & 0 deletions Example/Podfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
source 'https://github.com/CocoaPods/Specs.git'

use_frameworks!

target 'TinkoffIDExample' do
Expand Down
23 changes: 16 additions & 7 deletions Example/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
PODS:
- SnapKit (5.0.1)
- TinkoffID (1.1.0)
- TinkoffID/Tests (1.1.0)
- SnapKit (5.6.0)
- TCSSSLPinningPublic (4.0.0):
- TrustKit (= 1.6.3)
- TinkoffID (1.2.0):
- TCSSSLPinningPublic (~> 4.0)
- TinkoffID/Tests (1.2.0):
- TCSSSLPinningPublic (~> 4.0)
- TrustKit (1.6.3)

DEPENDENCIES:
- SnapKit
- TinkoffID (from `../`)
- TinkoffID/Tests (from `../`)

SPEC REPOS:
trunk:
https://github.com/CocoaPods/Specs.git:
- SnapKit
- TCSSSLPinningPublic
- TrustKit

EXTERNAL SOURCES:
TinkoffID:
:path: "../"

SPEC CHECKSUMS:
SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
TinkoffID: 5f8c16ed1ec6ab3c055d76b162d5770a324ad3db
SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
TCSSSLPinningPublic: 7d8aed728c4a1ff66feb2323f686a389b868f1e5
TinkoffID: c78e0033baeeefcf1c3782496fe4e1b65159f6d1
TrustKit: a2f0c3a926f0a3ce3c082db9a39f1f540dbb04cb

PODFILE CHECKSUM: ebbf3aa204a2c47033aee78dc783acf8bc5423bd
PODFILE CHECKSUM: afca0ae77c379b2ea123dd308359330458e3b198

COCOAPODS: 1.10.1
4 changes: 2 additions & 2 deletions Example/TinkoffIDExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = "$(SRCROOT)/App/Resources/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -378,7 +378,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = "$(SRCROOT)/App/Resources/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
61 changes: 54 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* [Отзыв авторизационных данных](#Отзыв-авторизационных-данных)
* [Структура TinkoffTokenPayload](#Структура-TinkoffTokenPayload)
* [Хранение Refresh Token](#Хранение-Refresh-Token)
* [Авторизация через WebView](#Авторизация-через-WebView)
* [UI](#UI)
* [Отладка без приложения Тинькофф](#Отладка-без-приложения-Тинькофф)
* [Настройка приложения](#Настройка-приложения)
Expand Down Expand Up @@ -65,6 +66,40 @@ pod 'TinkoffID'
<string>tinkoffbank</string>
</array>
```
+ Добавленная запись в `plist`, позволяющая Вашему приложению получать запасные сертификаты SSL Тинькофф.

```
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>certs.tinkoff.ru</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>tcsbank.ru</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>tinkoff.ru</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>

</dict>
</dict>
```

## Структура публичной части SDK

Expand All @@ -75,6 +110,7 @@ pod 'TinkoffID'
+ `ITinkoffAuthCallbackHandler` - обработчик возврата в приложение из приложения Тинькофф
+ `ITinkoffCredentialsRefresher` - объект, умеющий обновлять `Credentials` по их `Refresh token`
+ `ITinkoffSignOutInitiator` - инициатор отзыва авторизационных данных
+ `ITinkoffWebViewPresentationProvider` - провайдет источника для показа [WebView](#Авторизация-через-WebView)

В зависимости от архитектуры приложения можно использовать непосредственно`ITinkoffID` или каждый подпротокол отдельно в требуемой части системы.

Expand Down Expand Up @@ -197,6 +233,23 @@ tinkoffId.signOut(with: credentials.accessToken, tokenTypeHint: .access, complet

При получении `TinkoffTokenPayload` и наличии у него поля `refreshToken` имеет смысл сохранить значение этого поля чтобы иметь возможность запросить новый `accessToken`, когда прежний станет неактивным. Рекомендуемый способ хранения токена - [Keychain Services](https://developer.apple.com/documentation/security/keychain_services)

## Авторизация через WebView

В некоторых случаях система не открывает приложения по universal link, а ведет в браузер. Для того чтобы избежать этого, добавлен fallback на открытие WebView.

В данном сценарии при отсутствии установленного на телефоне приложения Тиннькофф или невозможности открыть приложение по universal link, SDK попробует открыть WebView с веб-авторизацией. Пользователю будет предложена авторизация по номеру телефону. После успешной авторизации WebView автоматически передаст управление SDK для продлолжения процесса.

Для использования необходимо установить значение `usesUniversalLinks` в `true` у `TargetAppConfiguration` (для `TinkoffApp` настроено по умолчанию), и передать провайдер `IAuthWebViewSourceProvider`.

Если же использовать дефолтную конфигурацию фабрики:
```
let factory = TinkoffIDFactory(
clientId: clientId,
callbackUrl: callbackUrl)
```
открытие WebView будет происходить от `keyWindow`.


## UI
SDK поставляет два варианта фирменных кнопок входа через Тинькофф.
Первый вариант - стандартная прямоугольная кнопка с текстом, с возможностью задать текст, радиус скругления и шрифт. Так же можно выбрать один из трех вариантов цветового стиля и размера. Есть возможность добавить дополнительный текст для привлечения клиентов.
Expand Down Expand Up @@ -299,13 +352,7 @@ SDK поставляется с примером приложения. Для з

### AuthViewController

`AuthViewController` инициируется ссылками на объекты, реализующими `ITinkoffAuthInitiator`, `ITinkoffCredentialsRefresher` и `ITinkoffSignOutInitiator` соответственно.

В текущей реализации все эти ссылки указывают на один и тот же экземпляр объекта `TinkoffID`, реализующий интерфейс `ITinkoffID`. Такой подход был выбран для демонстрации возможности использования подинтерфейсов `ITinkoffID` в той или иной части системы. Пользователь SDK вправе сам решать использовать ли ему единый интерфейс `ITinkoffID` или необходимый подинтерфейс в зависимости от архитектуры приложения.

Подробнее с подинтерфесами `ITinkoffID` можно ознакомиться в разделе `Структура публичной части SDK`.

После загрузки `view` контроллер добавляет на него кнопку входа через Тинькофф, по нажатию на которую будет инициирована авторизация.
`AuthViewController` инициируется ссылками на объекты, реализующими `ITinkoffAuthInitiator`, `ITinkoffCredentialsRefresher`, `ITinkoffSignOutInitiator` и `ITinkoffWebViewPresentationProvider` соответственно.

## Поддержка
Сообщать об ошибках и запрашивать новый функционал можно в разделе [Issues](https://github.com/tinkoff-mobile-tech/TinkoffID-iOS/issues)
Expand Down
67 changes: 67 additions & 0 deletions Sources/API/PinningDelegate/PinningDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// PinningDelegate.swift
// Pods-TinkoffIDExample
//
// Created by Aleksandr Moskalyuk on 18.05.2023.
//

import Foundation
import TCSSSLPinning
import WebKit

protocol IPinningDelegate {}

final class PinningDelegate: NSObject, IPinningDelegate {

// MARK: - Dependencies

private var httpPublicKeyPinningService: IHTTPPublicKeyPinningService

// MARK: - Lifestyle

init(hostAndPinsURL: String?) {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
let bundleID = Bundle.main.bundleIdentifier?.description ?? "ru.tinkoff.id"
let configuration = HPKPServiceConfiguration(hostAndPinsURL: URL(string: hostAndPinsURL ?? "") ?? HPKPServiceConstants.Configuration.productionHostAndPinsURL,
untrustedConnectionPolicy: .continue,
cachedHostsAndPinsDefaultsKey: "\(bundleID).hostsandpins",
appParameters: AppParameters(version: version ?? "1.0", origin: "origin"))
self.httpPublicKeyPinningService = HPKPServiceAssembly.createHPKPPinningService(with: configuration)

self.httpPublicKeyPinningService.configure()
self.httpPublicKeyPinningService.updateHostsAndPins()

super.init()
}
}

// MARK: - URLSessionDelegate

extension PinningDelegate: URLSessionDelegate {
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
httpPublicKeyPinningService.urlSession?(session, didBecomeInvalidWithError: error)
}

func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
httpPublicKeyPinningService.urlSession?(session,
didReceive: challenge,
completionHandler: completionHandler)

}

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
httpPublicKeyPinningService.urlSessionDidFinishEvents?(forBackgroundURLSession: session)
}
}

// MARK: - WKNavigationDelegate

extension PinningDelegate: WKNavigationDelegate {
public func webView(_ webView: WKWebView,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
httpPublicKeyPinningService.webView?(webView, didReceive: challenge, completionHandler: completionHandler)
}
}
2 changes: 1 addition & 1 deletion Sources/AppLaunching/IAppLauncher/IAppLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ protocol IAppLauncher {
var canLaunchApp: Bool { get }

/// Запускает приложение с заданными опциями
func launchApp(with options: AppLaunchOptions) throws
func launchApp(with options: AppLaunchOptions, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) throws
}
5 changes: 2 additions & 3 deletions Sources/AppLaunching/IAppLauncher/URLSchemeAppLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,10 @@ final class URLSchemeAppLauncher: IAppLauncher {
.map(router.canOpenURL) ?? false
}

func launchApp(with options: AppLaunchOptions) throws {
func launchApp(with options: AppLaunchOptions, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) throws {
let appUrl = try builder.buildUrlScheme(with: options)

if !router.open(appUrl) {
// Не удалось запустить приложение
if !router.openWithFallback(appUrl, universalLinksOnly: universalLinksOnly, completion: completion) {
throw Error.launchFailure
}
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/AppLaunching/IAppLauncher/Utils/Router/IURLRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ protocol IURLRouter {

/// Открывает заданный URL и возвращает `true` если открытие удалось
func open(_ url: URL) -> Bool

/// Открывает заданный URL c фоллбэком на открытие вебвью в случае если приложение не установлено
func openWithFallback(_ url: URL, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) -> Bool
}

extension UIApplication: IURLRouter {
Expand All @@ -36,4 +39,12 @@ extension UIApplication: IURLRouter {

return true
}

func openWithFallback(_ url: URL, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) -> Bool {
guard canOpenURL(url) else { return false }

open(url, options: [.universalLinksOnly : universalLinksOnly], completionHandler: completion)

return true
}
}
5 changes: 5 additions & 0 deletions Sources/Extensions/Environment+EnvironmentConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@
// limitations under the License.

import Foundation
import TCSSSLPinning

extension TinkoffEnvironment: EnvironmentConfiguration {
public var hostAndPinsUrl: String? {
return HPKPServiceConstants.Configuration.productionHostAndPinsURL.absoluteString
}

public var apiBaseUrl: String {
switch self {
case .production:
Expand Down
8 changes: 8 additions & 0 deletions Sources/Extensions/TinkoffApp+TargetAppConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@
import Foundation

extension TinkoffApp: TargetAppConfiguration {

public var urlScheme: String {
switch self {
case .bank:
return "tinkoffbank://"
}
}

public var usesUniversalLinks: Bool {
switch self {
case .bank:
return true
}
}

public var authUrl: String {
switch self {
case .bank:
Expand Down
Loading