swiftからsalesforceへOAuth認証してRestAPIへ接続する

iOSアプリからSalesforceとRestAPIで通信するためにOAuth認証してみた。

認証方法の種類

Salesforceへ接続するためにはいくつか方法があるが、

今回はPHPでも利用したことがある、ユーザー名パスワードフロー、を利用してみる。 たぶん、パスワードの保持などが必要など、セキュリティとしては一番低いものっぽいので、JWTなどで認証した方がいいのかもしれない。

SwiftのライブラリとしてCocoapodsで提供されている認証用ライブラリがあるが、このライブラリではWebサーバ認証フローが使われており、不特定多数との接続には向いていない。 (Webサーバ認証フローは、Salesforceのアカウントを取得している前提となる。Facebookログイン、のようなイメージ)

準備

Salesforce側へ接続するために必要になる情報は以下。 接続アプリケーションは事前に作成しておく。

  • ログインID
  • ログインパスワード
  • セキュリティトークン(IPアドレスを指定している場合は必要なし)
  • コンシューマ鍵 (クライアントID)
  • コンシューマの秘密 (クライアントシークレット)

環境変数

開発中はSandboxへ、リリース時には本番環境へ、と接続先を切り替えたいので、環境変数へ上記の内容を保存する。

まず、Build Settingsに値を追加する。

プロジェクトから、TARGETSファイルを選択。 Build Settingを選んで、プラスマークをクリックして、Add User-Defined Settingを選ぶ。

左側には設定したい項目名をKeyとして登録。 自動的に、DebugReleaseが表示されるので、それぞれの値を入れる。

次に、Info.plistに定義したキーを呼び出せるように設定する。 Infomation Property List+をクリックして追加する。

Keyは実際にコードから呼び出す際に使用する名前となる。 TypeStringとする。 Valueは定義した値を呼び出すため、${KEY}というように記述する。

ここまでで設定は完了。 以下のコードで呼び出せる。

1
let SF_LOGIN_URL = NSBundle.mainBundle().objectForInfoDictionaryKey("SF_LOGIN_URL") as! String

OAuth認証

準備した値を使用してアクセストークンを要求する。 アクセストークンの要求は、指定のエンドポイントへPOSTで値を送信する事で行う。

エンドポイント

送信する値

  • grant_type — password
  • client_id — 接続アプリケーション定義のコンシューマ鍵
  • client_secret — 接続アプリケーション定義のコンシューマの秘密
  • username — ユーザ名
  • password — パスワード (+セキュリティトークン)

パスワードはIPを指定していない場合は、パスワードの後ろにセキュリティトークンを繋げてやる必要がある。

POST送信

追記:HTTP通信とJSONのパースについてはライブラリを使ったバージョンの方がいいかも。
[swift]alamofireとswiftyjsonを使ってAPIからデータを取得する

POST送信は、NSURLConnectionがディスコンになったので、NSURLSessionを使用する必要があるとのこと。 非同期での通信が基本になったよう。

送信し、返ってきた値をJSONに変換して受け取る。 以下を参考にしてコードを記述した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let SF_LOGIN_URL = NSBundle.mainBundle().objectForInfoDictionaryKey("SF_LOGIN_URL") as! String
let SF_CLIENT_ID = NSBundle.mainBundle().objectForInfoDictionaryKey("SF_CLIENT_ID") as! String
let SF_CLIENT_SECRET = NSBundle.mainBundle().objectForInfoDictionaryKey("SF_CLIENT_SECRET") as! String
let SF_USERNAME = NSBundle.mainBundle().objectForInfoDictionaryKey("SF_USERNAME") as! String
let SF_PASSWORD = NSBundle.mainBundle().objectForInfoDictionaryKey("SF_PASSWORD") as! String

let URL = NSURL(string: SF_LOGIN_URL + "/services/oauth2/token")
let req = NSMutableURLRequest(URL: URL!)
req.HTTPMethod = "POST"
let paramString = "grant_type=password&client_id=" + SF_CLIENT_ID + "&client_secret=" + SF_CLIENT_SECRET + "&username=" + SF_USERNAME + "&password=" + SF_PASSWORD
req.HTTPBody = paramString.dataUsingEncoding(NSUTF8StringEncoding)

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration, delegate: nil, delegateQueue: NSOperationQueue.mainQueue())

let task = session.dataTaskWithRequest(req, completionHandler: {
    (let data, let response, let error) -> Void in
    do{
        let json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments) as! NSDictionary
        print(json)
    }catch{
        print("error")
    }
})
task.resume()

参考

ATS

しかしこのまま送信するとエラーが返ってくる。

1
2
2016-05-23 18:56:48.600 XXX-DEV[30024:28386036] CFNetwork SSLHandshake failed (-9824)
2016-05-23 18:56:48.600 XXX-DEV[30024:28386036] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824)

これは、iOS9から導入されたATSという機能によるものらしい。 外部への送信はHTTPSかつセキュアな証明書を使用していないと接続しない、というセキュリティ強化のための機能のよう。 本来はサーバー側で証明書を変更するなどの対応が好ましいが、salesforce側の証明書を変更するのは不可能なため、例外ドメインを指定して通信を許可してやる。

通信したいドメインがATSに対応しているかどうかは、nscurlコマンドを使用すると調べる事ができるよう。

このコマンドではATSの設定を変えながらしらみつぶしにチェックしてくれるコマンド、となっている。 なので、この結果をみながら接続がパスする設定を確認する。

1
$ nscurl --ats-diagnostics https://test.salesforce.com/

test.salesforce.comの場合は、 TLSのバージョンを変更しただけでは通信が失敗しているが、 Perfect Forward Secrecyfalseとした場合にはパスしていることがわかる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
================================================================================

Configuring TLS exceptions for test.salesforce.com

---
TLSv1.2
2016-05-23 19:06:14.743 nscurl[30351:28433837] CFNetwork SSLHandshake failed (-9824)
2016-05-23 19:06:14.744 nscurl[30351:28433837] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824)
Result : FAIL
---

---
TLSv1.1
2016-05-23 19:06:14.814 nscurl[30351:28433837] CFNetwork SSLHandshake failed (-9824)
2016-05-23 19:06:14.815 nscurl[30351:28433837] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824)
Result : FAIL
---

---
TLSv1.0
2016-05-23 19:06:14.883 nscurl[30351:28433837] CFNetwork SSLHandshake failed (-9824)
2016-05-23 19:06:14.883 nscurl[30351:28433837] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824)
Result : FAIL
---

================================================================================

Configuring PFS exceptions for test.salesforce.com

---
Disabling Perfect Forward Secrecy
Result : PASS
---

コマンドで--verboseオプションをつけると設定の内容を具体的に表示させる事ができる。

1
$ nscurl --ats-diagnostics --verbose https://test.salesforce.com/

以下は失敗。

1
2
3
4
5
6
7
{
    NSExceptionDomains =     {
        "test.salesforce.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.2";
        };
    };
}

以下は成功。

1
2
3
4
5
6
7
{
    NSExceptionDomains =     {
        "test.salesforce.com" =         {
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}

この結果からinfo.plistに設定を追加していく。

これでようやく通信に成功し、アクセストークンを取得することが出来た。

1
2
3
4
5
6
7
8
{
  "access_token":"(ACCESS_TOKEN)",
  "instance_url":"https://cs5.salesforce.com",
  "id":"https://test.salesforce.com/id/xxx/xxx",
  "token_type":"Bearer",
  "issued_at":"1463998466487",
  "signature":"(SIGNATURE)"
}

APIへ接続

今回は自作したApex Rest APIに接続してみる。 apexの作り方は割愛。エンドポイントはapex内で以下のように指定しているとする。

1
@RestResource(urlMapping='/user/login')

APIへの接続には、アクセストークンを取得した際に一緒に取得したinstance_urlへ接続する。 このURLは環境によって変わることがある。 しかしこのドメインもATSによって接続が失敗してしまう。 今回は簡易だが、ATSの機能自体をオフにすることで対応する。

NSAppTransportSecurityの下に、NSAllowsArbitraryLoadsを追加し、trueにしてやることで機能自体をオフにすることができる。

APIへ接続する際には、ヘッダにさきほど取得したアクセストークンを入れてやる。

1
req.addValue("Bearer \(self.access_token)", forHTTPHeaderField: "Authorization")

アクセストークンを取得した後からのコードは以下となる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//OAuth認証後に取得する
var access_token = json["access_token"] as? String
var instance_url = json["instance_url"] as? String
var token_type = json["token_type"] as? String

//APIへ接続
let URL = NSURL(string: self.instance_url + "/services/apexrest/user/login")
let req = NSMutableURLRequest(URL: URL!)

req.addValue("\(self.token_type) \(self.access_token)", forHTTPHeaderField: "Authorization") //ヘッダ

let user_email = "xxx@example.com"
let user_pass = "xxx"

req.HTTPMethod = "POST"
let paramString = "email=\(user_email)&passwd=\(user_pass)"
req.HTTPBody = paramString.dataUsingEncoding(NSUTF8StringEncoding)
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration, delegate: nil, delegateQueue: NSOperationQueue.mainQueue())

let task = session.dataTaskWithRequest(req, completionHandler: {
    (let data, let response, let error) -> Void in
    do{
        let json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments) as! NSDictionary
        print(json)
    }catch{
        print("error")
    }
})
task.resume()

これで指定のエンドポイントへ接続が出来た。

まとめ

とりあえず、アクセストークンを取得してRest APIへ接続、まで一通り出来た。 後は必要であれば、認証方式をJWTにしたり出来るといいかもしれない。

もしSFへの接続でいいライブラリなどがあれば教えて下さい。

   このエントリーをはてなブックマークに追加