Kotlin
OpenAPI Specificationから生成 されたKotlin用クライアントライブラリを使用するサンプルを示します。
サインイン
ウェブアプリケーション上でのユーザーのサインイン操作により認証を行い、APIにアクセスする場合のコードサンプルです。
このサンプルを実行するには以下の事前準備を行なってください。
intdashサーバーの管理者からOAuth2.0のクライアントIDを入手します。(例:
abcdefg123456
)これから開発するアプリケーションのコールバックスキーム名を決め、intdashサーバーの管理者へスキーム名の登録を依頼してください。(例:
companyname.appname
)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)
}
}