Skip to content

Commit a6222ba

Browse files
authored
Multiplatform adjustments (#252)
1 parent 2f85103 commit a6222ba

14 files changed

Lines changed: 284 additions & 29 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ written for Android. You can read more on the [Descope Website](https://descope.
99
Add the following to your `build.gradle` dependencies:
1010

1111
```groovy
12-
implementation 'com.descope:descope-kotlin:0.17.0'
12+
implementation 'com.descope:descope-kotlin:0.17.1'
1313
```
1414

1515
## Quickstart

descopesdk/src/main/java/com/descope/internal/http/SystemInfo.kt renamed to descopesdk/src/main/java/com/descope/android/SystemInfo.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
package com.descope.internal.http
1+
package com.descope.android
22

33
import android.content.Context
44
import android.os.Build
55

6-
internal interface SystemInfo {
6+
interface SystemInfo {
77
val appName: String?
88
val appVersion: String?
99
val platformVersion: String
1010
val device: String?
1111
}
1212

13-
internal class DescopeSystemInfo private constructor(context: Context) : SystemInfo {
13+
class DescopeSystemInfo private constructor(context: Context) : SystemInfo {
1414

1515
override val appName: String? = try {
1616
context.applicationInfo.loadLabel(context.packageManager).toString()

descopesdk/src/main/java/com/descope/internal/http/DescopeClient.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.descope.internal.http
22

3+
import com.descope.android.SystemInfo
34
import com.descope.sdk.DescopeConfig
45
import com.descope.sdk.DescopeSdk
56
import com.descope.types.DeliveryMethod

descopesdk/src/main/java/com/descope/internal/http/Responses.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.descope.internal.http
33
import com.descope.internal.others.optionalMap
44
import com.descope.internal.others.secToMs
55
import com.descope.internal.others.stringOrEmptyAsNull
6+
import com.descope.internal.others.toBooleanMap
67
import com.descope.internal.others.toObjectList
78
import com.descope.internal.others.toStringList
89
import org.json.JSONObject
@@ -54,6 +55,15 @@ internal data class UserResponse(
5455
val givenName: String?,
5556
val middleName: String?,
5657
val familyName: String?,
58+
val password: Boolean,
59+
val status: String,
60+
val roleNames: List<String>,
61+
val ssoAppIds: List<String>,
62+
val oauthProviders: Map<String, Boolean>,
63+
val webauthn: Boolean,
64+
val totp: Boolean,
65+
val saml: Boolean,
66+
val scim: Boolean,
5767
) {
5868
companion object {
5969
@Suppress("UNUSED_PARAMETER")
@@ -74,6 +84,15 @@ internal data class UserResponse(
7484
givenName = stringOrEmptyAsNull("givenName"),
7585
middleName = stringOrEmptyAsNull("middleName"),
7686
familyName = stringOrEmptyAsNull("familyName"),
87+
password = optBoolean("password"),
88+
status = getString("status"),
89+
roleNames = getJSONArray("roleNames").toStringList(),
90+
ssoAppIds = getJSONArray("ssoAppIds").toStringList(),
91+
oauthProviders = getJSONObject("OAuth").toBooleanMap(),
92+
webauthn = optBoolean("webauthn"),
93+
totp = optBoolean("TOTP"),
94+
saml = optBoolean("SAML"),
95+
scim = optBoolean("SCIM"),
7796
)
7897
}
7998
}

descopesdk/src/main/java/com/descope/internal/others/Utils.kt

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.descope.internal.others
22

33
import android.util.Base64
4+
import com.descope.types.DescopeException
45
import org.json.JSONArray
56
import org.json.JSONException
67
import org.json.JSONObject
@@ -15,14 +16,20 @@ internal fun ByteArray.toBase64(): String {
1516
return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
1617
}
1718

18-
// JSON
19+
// JSON -> Map/List
1920

2021
internal fun JSONObject.stringOrEmptyAsNull(key: String): String? = try {
2122
getString(key).ifEmpty { null }
2223
} catch (_: JSONException) {
2324
null
2425
}
2526

27+
internal fun JSONObject.booleanOrNull(key: String): Boolean? = try {
28+
getBoolean(key)
29+
} catch (_: JSONException) {
30+
null
31+
}
32+
2633
internal fun JSONObject.toMap(): Map<String, Any> {
2734
val map = mutableMapOf<String, Any>()
2835
keys().forEach { key ->
@@ -54,27 +61,66 @@ internal fun JSONObject.optionalMap(key: String): Map<String, Any> = try {
5461
emptyMap()
5562
}
5663

57-
internal fun JSONArray.toStringList(): List<String> {
58-
val list = mutableListOf<String>()
64+
internal fun JSONObject.toBooleanMap(): Map<String, Boolean> = toTypedMap<Boolean>()
65+
66+
internal fun JSONArray.toStringList(): List<String> = toTypedList<String>()
67+
68+
internal fun JSONArray.toStringSet(): Set<String> = toTypedSet<String>()
69+
70+
internal fun JSONArray.toObjectList(): List<JSONObject> = toTypedList<JSONObject>()
71+
72+
internal inline fun <reified T> JSONObject.toTypedMap(): Map<String, T> {
73+
val map = mutableMapOf<String, T>()
74+
keys().forEach { key ->
75+
map[key] = when(val obj = get(key)) {
76+
is T -> obj
77+
else -> throw DescopeException.decodeError.with(desc = "JSON object expected only '${T::class.java.name}' but contains an unexpected type: '${obj.javaClass.name}'")
78+
}
79+
}
80+
return map.toMap()
81+
}
82+
83+
internal inline fun <reified T> JSONArray.toTypedList(): List<T> {
84+
val list = mutableListOf<T>()
5985
for (i in 0 until length()) {
60-
list.add(getString(i))
86+
list.add(when(val obj = get(i)) {
87+
is T -> obj
88+
else -> throw DescopeException.decodeError.with(desc = "JSON array expected only '${T::class.java.name}' but contains an unexpected type: '${obj?.javaClass?.name}'")
89+
})
6190
}
6291
return list
6392
}
6493

65-
internal fun JSONArray.toObjectList(): List<JSONObject> {
66-
val list = mutableListOf<JSONObject>()
94+
internal inline fun <reified T> JSONArray.toTypedSet(): Set<T> {
95+
val set = mutableSetOf<T>()
6796
for (i in 0 until length()) {
68-
list.add(getJSONObject(i))
97+
set.add(when(val obj = get(i)) {
98+
is T -> obj
99+
else -> throw DescopeException.decodeError.with(desc = "JSON array expected only '${T::class.java.name}' but contains an unexpected type: '${obj?.javaClass?.name}'")
100+
})
69101
}
70-
return list
102+
return set
71103
}
72104

105+
// Map/List -> JSON
106+
73107
internal fun List<*>.toJsonArray(): JSONArray = JSONArray().apply {
74108
this@toJsonArray.forEach {
75109
when {
76110
it is Map<*, *> -> put(it.toJsonObject())
77111
it is List<*> -> put(it.toJsonArray())
112+
it is Set<*> -> put(it.toJsonArray())
113+
it != null -> put(it)
114+
}
115+
}
116+
}
117+
118+
internal fun Set<*>.toJsonArray(): JSONArray = JSONArray().apply {
119+
this@toJsonArray.forEach {
120+
when {
121+
it is Map<*, *> -> put(it.toJsonObject())
122+
it is List<*> -> put(it.toJsonArray())
123+
it is Set<*> -> put(it.toJsonArray())
78124
it != null -> put(it)
79125
}
80126
}
@@ -87,6 +133,7 @@ internal fun Map<*, *>.toJsonObject(): JSONObject = JSONObject().apply {
87133
when {
88134
value is Map<*, *> -> put(key, value.toJsonObject())
89135
value is List<*> -> put(key, value.toJsonArray())
136+
value is Set<*> -> put(key, value.toJsonArray())
90137
value != null -> put(key, value)
91138
}
92139
}

descopesdk/src/main/java/com/descope/internal/routes/OAuth.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ internal suspend fun nativeAuthorization(context: Context, responseJson: JSONObj
5454
}
5555

5656
internal suspend fun nativeAuthorization(context: Context, startResponse: OAuthNativeStartServerResponse): NativeAuthorizationResponse {
57-
if (!startResponse.implicit) {
57+
val identityToken = performNativeAuthorization(context, startResponse.clientId, startResponse.nonce, startResponse.implicit)
58+
return NativeAuthorizationResponse(startResponse.stateId, identityToken)
59+
}
60+
61+
suspend fun performNativeAuthorization(context: Context, clientId: String, nonce: String?, implicit: Boolean): String {
62+
if (!implicit) {
5863
throw DescopeException.oauthNativeFailed.with(message = "OAuth provider configuration must allow 'implicit' grant type")
5964
}
6065

61-
val authorization = performAuthorization(context, startResponse.clientId, startResponse.nonce)
62-
val identityToken = parseCredential(authorization.credential)
63-
return NativeAuthorizationResponse(startResponse.stateId, identityToken)
66+
val authorization = performAuthorization(context, clientId, nonce)
67+
return parseCredential(authorization.credential)
6468
}
6569

6670
private suspend fun performAuthorization(context: Context, clientId: String, nonce: String?): GetCredentialResponse {

descopesdk/src/main/java/com/descope/internal/routes/Passkey.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,15 @@ private fun convertOptions(options: String): String {
8888
return publicKey
8989
}
9090

91-
@RequiresApi(Build.VERSION_CODES.P)
92-
internal fun getPackageOrigin(context: Context): String {
93-
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
94-
val signers = packageInfo.signingInfo?.apkContentsSigners // nullable according to source code
91+
fun getPackageOrigin(context: Context): String {
92+
@Suppress("DEPRECATION")
93+
val signers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
94+
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
95+
packageInfo?.signingInfo?.apkContentsSigners // nullable according to source code
96+
} else {
97+
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
98+
packageInfo.signatures
99+
}
95100

96101
if (signers.isNullOrEmpty()) {
97102
throw DescopeException.passkeyFailed.with(message = "Failed to find signing certificates")
@@ -109,7 +114,7 @@ internal fun getPackageOrigin(context: Context): String {
109114
}
110115

111116
@SuppressLint("PublicKeyCredential")
112-
internal suspend fun performRegister(context: Context, options: String): String {
117+
suspend fun performRegister(context: Context, options: String): String {
113118
val publicKey = convertOptions(options)
114119
val request = CreatePublicKeyCredentialRequest(publicKey)
115120

@@ -134,7 +139,7 @@ internal suspend fun performRegister(context: Context, options: String): String
134139
}
135140
}
136141

137-
internal suspend fun performAssertion(context: Context, options: String): String {
142+
suspend fun performAssertion(context: Context, options: String): String {
138143
val publicKey = convertOptions(options)
139144
val option = GetPublicKeyCredentialOption(publicKey)
140145
val request = GetCredentialRequest(listOf(option))
@@ -160,3 +165,4 @@ internal suspend fun performAssertion(context: Context, options: String): String
160165
throw DescopeException.passkeyFailed.with(message = "Unexpected failure", cause = e)
161166
}
162167
}
168+

descopesdk/src/main/java/com/descope/internal/routes/Shared.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ internal fun UserResponse.convert(): DescopeUser = DescopeUser(
3636
givenName = givenName,
3737
middleName = middleName,
3838
familyName = familyName,
39+
status = DescopeUser.Status.deserialize(status),
40+
authentication = DescopeUser.Authentication(
41+
passkey = webauthn,
42+
password = password,
43+
totp = totp,
44+
oauth = oauthProviders.keys.toSet(),
45+
sso = saml,
46+
scim = scim,
47+
),
48+
authorization = DescopeUser.Authorization(
49+
roles = roleNames.toSet(),
50+
ssoAppIds = ssoAppIds.toSet(),
51+
),
52+
isUpdateRequired = false
3953
)
4054

4155
internal fun JwtServerResponse.convert(): AuthenticationResponse {

descopesdk/src/main/java/com/descope/sdk/Sdk.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ package com.descope.sdk
55
import android.content.Context
66
import android.content.pm.ApplicationInfo
77
import android.os.Looper
8+
import com.descope.android.DescopeSystemInfo
89
import com.descope.internal.http.DescopeClient
9-
import com.descope.internal.http.DescopeSystemInfo
1010
import com.descope.internal.others.ConsoleLogger
1111
import com.descope.internal.routes.Auth
1212
import com.descope.internal.routes.EnchantedLink
@@ -78,6 +78,6 @@ class DescopeSdk(context: Context, projectId: String, configure: DescopeConfig.(
7878
const val NAME = "DescopeAndroid"
7979

8080
/** The Descope SDK version */
81-
const val VERSION = "0.17.0"
81+
const val VERSION = "0.17.1"
8282
}
8383
}

descopesdk/src/main/java/com/descope/session/Storage.kt

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import androidx.core.content.edit
55
import androidx.core.net.toUri
66
import androidx.security.crypto.EncryptedSharedPreferences
77
import androidx.security.crypto.MasterKeys
8+
import com.descope.internal.others.booleanOrNull
89
import com.descope.internal.others.debug
910
import com.descope.internal.others.error
1011
import com.descope.internal.others.optionalMap
1112
import com.descope.internal.others.stringOrEmptyAsNull
1213
import com.descope.internal.others.toJsonArray
1314
import com.descope.internal.others.toJsonObject
1415
import com.descope.internal.others.toStringList
16+
import com.descope.internal.others.toStringSet
1517
import com.descope.internal.others.tryOrNull
1618
import com.descope.sdk.DescopeLogger
1719
import com.descope.types.DescopeUser
@@ -128,7 +130,7 @@ class SessionStorage(context: Context, private val projectId: String, logger: De
128130
get() = JSONObject().apply {
129131
put("sessionJwt", sessionJwt)
130132
put("refreshJwt", refreshJwt)
131-
put("user", user.toJson())
133+
put("user", user.serialize())
132134
}.toString()
133135

134136
companion object {
@@ -171,7 +173,25 @@ class EncryptedSharedPrefs(name: String, context: Context) : SessionStorage.Stor
171173

172174
// Serialization
173175

174-
private fun deserializeDescopeUser(json: JSONObject): DescopeUser = json.run {
176+
internal fun deserializeDescopeUser(json: JSONObject): DescopeUser = json.run {
177+
val authentication = optJSONObject("authentication").let { json ->
178+
DescopeUser.Authentication(
179+
password = json?.optBoolean("password") ?: false,
180+
passkey = json?.optBoolean("passkey") ?: false,
181+
totp = json?.optBoolean("totp") ?: false,
182+
oauth = json?.optJSONArray("oauth")?.toStringSet() ?: emptySet(),
183+
sso = json?.optBoolean("sso") ?: false,
184+
scim = json?.optBoolean("scim") ?: false,
185+
)
186+
}
187+
188+
val authorization = optJSONObject("authorization").let { json ->
189+
DescopeUser.Authorization(
190+
roles = json?.optJSONArray("roles")?.toStringSet() ?: emptySet(),
191+
ssoAppIds = json?.optJSONArray("ssoAppIds")?.toStringSet() ?: emptySet(),
192+
)
193+
}
194+
175195
DescopeUser(
176196
userId = getString("userId"),
177197
loginIds = getJSONArray("loginIds").toStringList(),
@@ -186,10 +206,14 @@ private fun deserializeDescopeUser(json: JSONObject): DescopeUser = json.run {
186206
givenName = stringOrEmptyAsNull("givenName"),
187207
middleName = stringOrEmptyAsNull("middleName"),
188208
familyName = stringOrEmptyAsNull("familyName"),
209+
status = DescopeUser.Status.deserialize(stringOrEmptyAsNull("status") ?: "enabled"),
210+
authentication = authentication,
211+
authorization = authorization,
212+
isUpdateRequired = booleanOrNull("isUpdateRequired") ?: true, // if the flag doesn't exist we've got old data without the new fields
189213
)
190214
}
191215

192-
private fun DescopeUser.toJson() = JSONObject().apply {
216+
internal fun DescopeUser.serialize() = JSONObject().apply {
193217
put("userId", userId)
194218
put("loginIds", loginIds.toJsonArray())
195219
put("createdAt", createdAt)
@@ -203,8 +227,25 @@ private fun DescopeUser.toJson() = JSONObject().apply {
203227
put("givenName", givenName)
204228
put("middleName", middleName)
205229
put("familyName", familyName)
230+
put("authentication", authentication.toJsonObject())
231+
put("authorization", authorization.toJsonObject())
232+
put("status", status.serialize())
233+
put("isUpdateRequired", isUpdateRequired)
206234
}
207235

236+
private fun DescopeUser.Authentication.toJsonObject() = JSONObject().apply {
237+
put("password", password)
238+
put("passkey", passkey)
239+
put("totp", totp)
240+
put("oauth", oauth.toJsonArray())
241+
put("sso", sso)
242+
put("scim", scim)
243+
}
244+
245+
private fun DescopeUser.Authorization.toJsonObject() = JSONObject().apply {
246+
put("roles", roles.toJsonArray())
247+
put("ssoAppIds", ssoAppIds.toJsonArray())
248+
}
208249
private fun createEncryptedStore(context: Context, projectId: String, logger: DescopeLogger?): SessionStorage.Store {
209250
try {
210251
val storage = EncryptedSharedPrefs(projectId, context)

0 commit comments

Comments
 (0)