Kotlin

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

サインイン

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

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

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

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

  3. AndroidManifest.xml で、以下のようにコールバックスキームを登録し、設定を記述します。

例)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Permissions -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application>

        ...

        <activity android:name="example.com.ExampleActivity"
            android:exported="true"
            android:launchMode="singleTask">
            <!--for WebAuthn when use browser app-->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="companyname.appname" android:host="oauth2" android:path="/callback" />
            </intent-filter>
        </activity>
    </application>
</manifest>

実装例は以下のとおりです。

class ExampleActivity : AppCompatActivity() {

    companion object {
        val TAG = ExampleActivity.javaClass.name

        // intdashサーバー名
        const val TARGET_SERVER = "https://example.com"
        // OAuth2.0 クライアントID
        const val INTDASH_CLIENT_ID = "abcdefg123456"
        // コールバックスキームを使用したコールバックURL
        const val CALLBACK_URL_SCHEME = "companyname.appname://oauth2/callback"
    }

    private var targetState: String = ""
    private var targetCodeVerifier: String = ""

    var apiClient = ApiClient()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_example)
        // クライアントに接続先を伝えます
        apiClient.setBasePath(TARGET_SERVER +"/api")
    }

    fun signIn() {
        var basePath = TARGET_SERVER + "/api"
        var authData = generateOAuth2AuthorizationUrl(basePath, INTDASH_CLIENT_ID, CALLBACK_URL_SCHEME)
        val authUrl = authData.first
        var codeVerifier = authData.second
        var state = authData.third
        var authEx = authData.fourth
        if (authEx != null || authUrl.isNullOrEmpty() || codeVerifier.isNullOrEmpty() || state.isNullOrEmpty()) {
            Log.e(TAG, "generateOAuth2AuthorizationUrl failed. " + (authEx?.localizedMessage ?: ""))
            return
        }
        targetCodeVerifier = codeVerifier
        targetState = state

        var i = Intent(Intent.ACTION_VIEW)
        i.data = Uri.parse(authUrl)
        startActivity(i)
    }

    override fun onNewIntent(intent: Intent?) {
        Log.d(TAG, "onNewIntent")
        super.onNewIntent(intent)
        if (intent == null) return
        var action = intent.action
        if (!action.equals(Intent.ACTION_VIEW)) return
        var uri = intent.data ?: return

        if (uri.path != "/callback") return
        var state = uri.getQueryParameter("state") ?: return
        var code = uri.getQueryParameter("code")
        if (state == targetState && !code.isNullOrEmpty()) {
            Log.d(TAG,"Web authentication was successful.")
            Thread {
                // 外部認証で取得した認証コードを使用してアクセストークンを取得します。
                var (response, error) = requestAccessToken(code)
                var accessToken = response?.accessToken
                if (accessToken == null) {
                    Log.e(TAG, "requestAccessToken failed. ${error?.javaClass?.name ?: ""}")
                    error?.printStackTrace()
                    return@Thread
                }
                // 認証成功
                Log.d(TAG, "Successful sign-in. accessToken: $accessToken")
                // ToDo: 取得したアクセストークンには期限が存在するので必要に応じてトークンのリフレッシュを行なってください。
            }.start()
        } else {
            Log.d(TAG, "Web authentication was failed.")
        }
    }

    private fun requestAccessToken(code: String) : Pair<IssueToken200Response?, Exception?> {
        try {
            var api = AuthOAuth2Api(apiClient)
            var res = api.issueToken(
                "authorization_code", // grantType
                "", // refreshToken
                "", // tenantUuid
                "", // username,
                "", // password
                INTDASH_CLIENT_ID, // clientId
                "", // clientSecret
                "", // clientCertification
                CALLBACK_URL_SCHEME, // redirectUri
                targetCodeVerifier, // codeVerifier
                code // code
            )
            return Pair(res, null)
        }
        catch (e: Exception) {
            return Pair(null, e)
        }
    }

}

/**
 * 外部認証を行うURLを生成する
 */
fun generateOAuth2AuthorizationUrl(basePath: String, clientId: String, callbackUrlScheme: String) : Quadruple<String?, String?, String?, Exception?> {
    try {
        var codeVerifier = generateCodeVerifier()
        var codeChallenge = convertCodeChallenge(codeVerifier)
        var state = generateState()
        var url = basePath +
                "/auth/oauth2/authorization" +
                "?client_id=" + clientId +
                "&response_type=" + "code" +
                "&redirect_uri=" + callbackUrlScheme +
                "&state=" + state +
                "&code_challenge=" + codeChallenge +
                "&code_challenge_method=" + "S256" +
                "&prompt=" + "login"
        return Quadruple(url, codeVerifier, state, null)
    }
    catch (e: Exception) {
        return Quadruple(null, null, null, e)
    }
}

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

    var randomString = ""
    var random = SecureRandom()
    for (i in 0 until length) {
        var index = random.nextInt(targetCharacters.length)
        randomString += 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
 */
fun generateCodeVerifier(length: Int = 128) : String {
    var unreservedCharacters =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
                "abcdefghijklmnopqrstuvwxyz" +
                "0123456789" +
                "-" + "." + "_" + "~"

    var randomString = ""
    var random = SecureRandom()
    for (i in 0 until length) {
        var index = random.nextInt(unreservedCharacters.length)
        randomString += 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.
 */
fun convertCodeChallenge(codeVerifier: String) : String {
    var ascii = codeVerifier.toByteArray(Charsets.US_ASCII)
    var hash = ascii.sha256()
    return hash.base64UrlEncodedString()
}

private fun ByteArray.sha256(): ByteArray {
    val md = MessageDigest.getInstance("SHA-256")
    return md.digest(this)
}

private fun ByteArray.base64UrlEncodedString() : String {
    var base64 = Base64.encodeToString(this, Base64.DEFAULT)
    val base64url = base64
        .replace("=", "").replace("[\n\r]".toRegex(), "") //.trimEnd('=') <- Not working.
        .replace('+', '-')
        .replace('/', '_')
    return base64url
}

data class Quadruple<out A, out B, out C, out D>(
    val first: A,
    val second: B,
    val third: C,
    val fourth: D
)

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

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

fun ExampleActivity.apiAccessExample(accessToken: String) {
    // 取得したアクセストークンを以下のようにセットしてください。
    // 以降、トークンの有効期限が切れない限り再設定は不要です。
    apiClient.setBearerToken(accessToken)
    // 任意のAPIを実行します。
    try {
        var api = AuthMeApi(apiClient)
        var user = api.me
        Log.d(TAG, "User name: ${user.name}, uuid: ${user.uuid}")
    }
    catch (exception: Exception) {
        Log.e(TAG, "Failed to getMe access. ${exception.javaClass.name}")
        exception.printStackTrace()
    }
}

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

fun ExampleActivity.requestListDataPoints(name: String, filters: List<String>?, start: ZonedDateTime?, end: ZonedDateTime?, limit: Long? = null, order: String? = null) : Pair<List<DataPointNormal>?, Exception?> {
    try {
        var api = MeasDataPointsApi(apiClient)
        var sStart: String? = null
        start?.let { date ->
            sStart = "${date.withZoneSameInstant(ZoneId.of("UTC")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))}.${date.nano/1000}Z"
        }
        var sEnd: String? = null
        end?.let { date ->
            sEnd = "${date.withZoneSameInstant(ZoneId.of("UTC")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))}.${date.nano/1000}Z"
        }
        var file: File? = null
        try {
            file = api.listDataPoints(
                name, // name
                sStart, // start
                sEnd, // end
                filters, // idq
                null, // since
                null, // exitOnError
                null, // label
                null, // period
                limit, // limit
                order, // order
                "rfc3339" // timeFormat
            )
            var points: MutableList<DataPointNormal> = mutableListOf()
            BufferedReader(FileReader(file)).use { br ->
                var json: String?
                while (br.readLine().also { json = it } != null) {
                    try {
                        //var point = DataPointNormal.fromJson(json) // Not working.
                        var point = DataPointNormal()
                        var obj = JSONObject(json)
                        point.time = DataPointTime(obj.getString("time"))
                        point.dataType = obj.getString("data_type")
                        point.dataId = obj.getString("data_id")
                        point.data = obj.getString("data")
                        point.createdAt = obj.getString("created_at")
                        points.add(point)
                    }
                    catch (e: Exception) {
                        Log.e(this.javaClass.name, "Failed to parse json. ${e.javaClass.name}, text: $json")
                        e.printStackTrace()
                        continue
                    }
                }
            }
            return Pair(points, null)
        }
        finally {
            // リクエストで生成されたファイルを必要に応じて削除します。
            file?.delete()
        }
    }
    catch (e: Exception) {
        return Pair(null, e)
    }
}