Swift

OpenAPI Specificationから生成 されたSwift用クライアントライブラリを使用するサンプルを示します。

サインインしてAPIにアクセスする

ウェブアプリケーション上でのユーザーのサインイン操作により認証を行い、APIにアクセスする場合のコードサンプル(iOS用とmacOS用)です。

このサンプルを実行するには以下の事前準備を行なってください。

  1. intdashサーバーの管理者からOAuth2.0のクライアントIDを入手します。(例: abcdefg123456

  2. これから開発するアプリケーションのコールバックスキーム名を決め、intdashサーバーの管理者へスキーム名の登録を依頼してください。(例: companyname.appname

  3. (iOSの場合のみ) Info.plistURL Types で、以下のようにコールバックスキームを登録します。

    例)

    Key

    Type

    Value

    URL types

    Array

      Item 0 (Viewer)

    Dictionary

        Document Role

    String

    Viewer

        URL identifier

    String

    $(PRODUCT_BUNDLE_IDENTIFIER)

        URL Schemes

    Array

          Item 0

    String

    スキーム名(例:companyname.appname)

実装例は以下のとおりです。iOSとmacOSとで必要な実装が異なります。

iOS/macOS共通部分

// APICode.swift

import Foundation
import intdash
import CommonCrypto

/// 外部認証を行うURLを生成する
public func generateOAuth2AuthorizationURL(clientId: String, callbackURLScheme: String, completion: @escaping (_ url: String?, _ codeVerifier: String? , _ state: String?, _ error: Error?)->()) {
    let codeVerifier = generateCodeVerifier()
    guard let codeChallenge = convertCodeChallenge(codeVerifier: codeVerifier) else {
        completion(nil, nil, nil, NSError(domain: #function, code: #line, userInfo: nil))
        return
    }
    let state = generateState()
    let res = AuthOAuth2API.oauth2AuthorizationWithRequestBuilder(
        clientId: clientId,
        responseType: .code,
        redirectUri: callbackURLScheme,
        state: state,
        codeChallenge: codeChallenge,
        codeChallengeMethod: .s256)
    // 生成されURLに `prompt=login` に付与することでキャッシュによるサインイン画面表示後のリダイレクトを防ぐことができます。
    let url = res.URLString + "&prompt=login"
    completion(url, codeVerifier, state, nil)
}

public func requestAccessToken(clientId: String, code: String, codeVerifier: String, callbackURLScheme: String, tenantUuid: String? = nil, completion: @escaping (_ response: InlineResponse200?, _ error: Error?)->()) {
    AuthOAuth2API.issueToken(
        grantType: .authorizationCode,
        tenantUuid: tenantUuid,
        clientId: clientId,
        redirectUri: callbackURLScheme,
        codeVerifier: codeVerifier,
        code: code, completion: completion)
}

/// stateを生成する
///
/// From [Section 10 of [RFC6749]](https://tools.ietf.org/html/rfc6749#section-10.12)
func generateState(_ length: Int = 32) -> String {
    let targetCharacters =
    "abcdefghijklmnopqrstuvwxyz" +
    "0123456789"

    var randomString = ""
    for _ in 0..<length {
        let random = arc4random_uniform(UInt32(targetCharacters.count))
        let index = targetCharacters.index(targetCharacters.startIndex, offsetBy: Int(random))
        randomString += String(targetCharacters[index])
    }
    return randomString
}

/// code_verifier を生成する
///
/// From [Section 4.1 of [RFC7636]](https://tools.ietf.org/html/rfc7636#section-4.1)
///
/// code_verifier = high-entropy cryptographic random STRING using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
/// from [Section 2.3 of [RFC3986]](https://tools.ietf.org/html/rfc3986#section-2.3), with a minimum length of 43 characters and a maximum length of 128 characters.
func generateCodeVerifier(_ length: Int = 128) -> String {
    let unreservedCharacters =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
    "abcdefghijklmnopqrstuvwxyz" +
    "0123456789" +
    "-" + "." + "_" + "~"

    var randomString = ""
    for _ in 0..<length {
        let random = arc4random_uniform(UInt32(unreservedCharacters.count))
        let index = unreservedCharacters.index(unreservedCharacters.startIndex, offsetBy: Int(random))
        randomString += String(unreservedCharacters[index])
    }
    return randomString
}

/// code_verifier を code_challenge に変換する
///
/// From [Section 4.2 of [RFC7636]](https://tools.ietf.org/html/rfc7636#section-4.2)
///
/// code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
/// Only supports S256, with a minimum length of 43 characters and a maximum length of 128 characters.
func convertCodeChallenge(codeVerifier: String) -> String? {
    guard let ascii = codeVerifier.data(using: .ascii) else { return nil }
    let hash = ascii.sha256
    return hash.base64URLEncodedString()
}

fileprivate extension Data {

    var sha256: Data {
        var digest = [UInt8].init(repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        self.withUnsafeBytes { bytes in
            _ = CC_SHA256(bytes.baseAddress, CC_LONG(self.count), &digest)
        }
        return Data(digest)
    }

    func base64URLEncodedString() -> String {
        let base64 = base64EncodedString()
        let base64url = String(base64
            .dropLast()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_"))
            .replacingOccurrences(of: "=", with: "")
        return base64url
    }

}

iOS用

// ExampleViewController.swift

import UIKit
import AuthenticationServices
import intdash

// intdashサーバー名
let kTargetServer: String = "https://example.com"
// OAuth2.0 クライアントID
let kIntdashClientId: String = "abcdefg123456"
// コールバックスキームを使用したコールバックURL
let kCallbackURLScheme: String = "companyname.appname://oauth2/callback"

class ExampleViewController: UIViewController {

    private var webAuthSession: NSObject?

    override func viewDidLoad() {
        super.viewDidLoad()
        // クライアントに接続先を伝えます。
        intdashAPI.basePath = kTargetServer + "/api"
    }

    func signIn() {
        DispatchQueue.global().async {
            // 外部認証を行うURLを生成します。
            generateOAuth2AuthorizationURL(clientId: kIntdashClientId, callbackURLScheme: kCallbackURLScheme) { url, codeVerifier, state, error in
                print("generateOAuth2AuthorizationURL: \(url ?? "")")
                guard error == nil, let url = url, let authURL = URL(string: url), let codeVerifier = codeVerifier else {
                    print("generateAuthorizationURL failed. \(error?.localizedDescription ?? "")")
                    return
                }
                DispatchQueue.main.async { [weak self] in
                    guard let self = self else { return }
                    let callbackURLScheme = kCallbackURLScheme.replacingOccurrences(of: ":", with: "%3A").replacingOccurrences(of: "/", with: "%2F") // URLエンコード
                    let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: callbackURLScheme) { [weak self] (callbackURL, error) in
                        guard let self = self else { return }
                        guard error == nil, let callbackURL = callbackURL else {
                            print("Web authentication callback error. \(error?.localizedDescription ?? "")")
                            return
                        }
                        var result = false
                        var code: String?
                        // レスポンスに含まれる認証コードを取得します。
                        if let queryItems = URLComponents(string: callbackURL.absoluteString)?.queryItems {
                            for item in queryItems {
                                print("[\(item.name)] => \(item.value ?? "NULL")")
                                if item.name == "state", item.value == state {
                                    result = true
                                }
                                if item.name == "code" {
                                    code = item.value
                                }
                            }
                        }
                        if let code = code, result {
                            print("Web authentication was successful.")
                            // 外部認証で取得した認証コードを使用してアクセストークンを取得します。
                            requestAccessToken(clientId: kIntdashClientId, code: code, codeVerifier: codeVerifier, callbackURLScheme: kCallbackURLScheme) { [weak self]  (response, error) in
                                guard let accessToken = response?.accessToken else {
                                    print("requestAccessToken failed. \(error?.localizedDescription ?? "")")
                                    return
                                }
                                // 認証成功。
                                print("Successful sign-in. accessToken: \(accessToken)")
                                // ToDo: 取得したアクセストークンには期限が存在するので必要に応じてトークンのリフレッシュを行なってください。
                            }
                        } else {
                            print("Web authentication failed.")
                        }
                    }
                    if #available(iOS 13.0, *) {
                        session.presentationContextProvider = self
                        session.prefersEphemeralWebBrowserSession = false
                    }
                    self.webAuthSession = session
                    // ウェブアプリケーションによる認証を開始します。
                    session.start()
                }
            }
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.signIn()
    }

}


extension ExampleViewController: ASWebAuthenticationPresentationContextProviding {

    @available(iOS 12.0, *)
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        return UIApplication.shared.windows.first ?? ASPresentationAnchor()
    }

}

macOS用

// ViewController.swift

import Cocoa
import WebKit
import intdash

// intdashサーバー名
let kTargetServer: String = "https://example.com"
// OAuth2.0 クライアントID
let kIntdashClientId: String = "abcdefg123456"
// コールバックスキームを使用したコールバックURL
let kCallbackURLScheme: String = "companyname.appname://oauth2/callback"

class ExampleViewController: NSViewController {

    // WKWebViewを用いて認証を行います。
    @IBOutlet weak var webView: WKWebView!

    private var targetState: String?
    private var targetCodeVerifier: String?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        // クライアントに接続先を伝えます。
        intdashAPI.basePath = kTargetServer + "/api"
        // WKWebViewのデリゲートをセットします。
        self.webView.navigationDelegate = self
    }

    func signIn() {
        // 外部認証を行うURLを生成します。
        generateOAuth2AuthorizationURL(clientId: kIntdashClientId, callbackURLScheme: kCallbackURLScheme) { [weak self] (url, codeVerifier, state, error) in
            guard error == nil, let url = url, let authURL = URL(string: url), let codeVerifier = codeVerifier else {
                print("generateAuthorizationURL failed. \(error?.localizedDescription ?? "")")
                return
            }
            self?.targetState = state
            self?.targetCodeVerifier = codeVerifier
            DispatchQueue.main.async {
                // ウェブアプリケーションによる認証を開始します。
                self?.webView.load(URLRequest(url: authURL))
            }
        }
    }

    override func viewDidAppear() {
        super.viewDidAppear()
        self.signIn()
    }

}

extension ExampleViewController : WKNavigationDelegate {

    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {}

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {}

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        defer { decisionHandler(.allow) }
        guard navigationAction.request.url?.path == "/callback" else { return }
        let callbackURL = navigationAction.request.url
        guard let callbackURL = callbackURL else { return }
        var result = false
        var code: String?
        // レスポンスに含まれる認証コードを取得します。
        if let queryItems = URLComponents(string: callbackURL.absoluteString)?.queryItems {
            for item in queryItems {
                print("[\(item.name)] => \(item.value ?? "NULL")")
                if item.name == "state", item.value == self.targetState {
                    result = true
                }
                if item.name == "code" {
                    code = item.value
                }
            }
        }
        if let code = code, result, let codeVerifier = self.targetCodeVerifier {
            print("Web authentication was successful.")
            DispatchQueue.global().async {
                // 外部認証で取得した認証コードを使用してアクセストークンを取得します。
                requestAccessToken(clientId: kIntdashClientId, code: code, codeVerifier: codeVerifier, callbackURLScheme: kCallbackURLScheme) { [weak self] (response, error) in
                    guard let accessToken = response?.accessToken else {
                        print("requestAccessToken failed. \(error?.localizedDescription ?? "")")
                        return
                    }
                    // 認証成功
                    print("Successful sign-in. accessToken: \(accessToken)")
                    // ToDo: 取得したアクセストークンには期限が存在するので必要に応じてトークンのリフレッシュを行なってください。
                }
            }
        }
    }
}

APIトークンを使用してAPIにアクセスする

APIトークンを使用して認証を行う場合は、以下のようにします。

import Foundation
import intdash

extension ExampleViewController {

    func apiAccessExample(accessToken: String) {
        // 取得したアクセストークンを以下のようにセットしてください。
        // 以降、トークンの有効期限が切れない限り再設定は不要です。
        intdashAPI.customHeaders["Authorization"] = "Bearer \(accessToken)"
        // 任意のAPIを実行します。
        AuthMeAPI.getMe { [weak self] user, error in
            guard let user = user else  {
                print("Failed to getMe access. \(error?.localizedDescription ?? "")")
                return
            }
            print("User name: \(user.name), uuid: \(user.uuid)")
        }
    }
}

過去の計測のデータポイントを取得する

import Foundation
import intdash

extension ExampleViewController {

    func requestListDataPoints(name: String, idq: [String]?, start: Date, end: Date, limit: Int64? = nil, order: MeasDataPointsAPI.Order_listDataPoints? = nil, completion: @escaping ([DataPointNormal]?, Error?)->()) {
        // 以下でDateをRFC3339の文字列形式に変換します。
        // DateFormatterではマイクロ秒以下は出力できないのでご注意ください。
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
        let sStart = formatter.string(from: start)
        let sEnd = formatter.string(from: end)
        // MeasDataPointsAPI.listDataPointsAPIにアクセスします。
        MeasDataPointsAPI.listDataPoints(name: name, start: sStart, end: sEnd, idq: idq, limit: limit, order: order, timeFormat: .rfc3339) { url, err in
            guard let url = url else {
                completion(nil, err)
                return
            }
            do {
                // レスポンスはファイルに出力されます。
                let str = try String.init(contentsOf: url, encoding: .utf8)
                let lines = str.components(separatedBy: "\n")
                let decoder = JSONDecoder()
                var points = [DataPointNormal]()
                for json in lines {
                    guard let data = json.data(using: .utf8), let point = try? decoder.decode(DataPointNormal.self, from: data) else { continue }
                    points.append(point)
                }
                // リクエストで生成されたファイルを必要に応じて削除します。
                try? FileManager.default.removeItem(at: url)
                completion(points, nil)
            } catch {
                completion(nil, error)
            }
        }
    }
}