From bf533c0f00515f44c3cc6de0ce3bce6924ba2f68 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 29 Nov 2023 14:28:40 +0800 Subject: [PATCH] feat: support organizations (#221) --- .../io/logto/sdk/android/LogtoClient.kt | 82 +++++++++++++++---- .../sdk/android/exception/LogtoException.kt | 2 + .../io/logto/sdk/android/type/LogtoConfig.kt | 15 +++- .../io/logto/sdk/android/LogtoClientTest.kt | 20 +++-- .../logto/sdk/android/type/LogtoConfigTest.kt | 12 +++ .../src/main/kotlin/io/logto/sdk/core/Core.kt | 11 +++ .../io/logto/sdk/core/constant/ClaimName.kt | 3 + .../io/logto/sdk/core/constant/QueryKey.kt | 1 + .../sdk/core/constant/ReservedResource.kt | 5 ++ .../io/logto/sdk/core/constant/UserScope.kt | 28 +++++++ .../logto/sdk/core/extension/JwtClaimsExt.kt | 3 + .../io/logto/sdk/core/type/IdTokenClaims.kt | 33 ++++++++ .../kotlin/io/logto/sdk/core/CoreFetchTest.kt | 4 +- .../logto/sdk/core/constant/ConstantTest.kt | 1 + .../sdk/core/extension/JwtClaimsExtKtTest.kt | 9 ++ .../logto/sdk/core/type/IdTokenClaimsTest.kt | 9 ++ 16 files changed, 214 insertions(+), 24 deletions(-) create mode 100644 kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/ReservedResource.kt diff --git a/android-sdk/android/src/main/kotlin/io/logto/sdk/android/LogtoClient.kt b/android-sdk/android/src/main/kotlin/io/logto/sdk/android/LogtoClient.kt index 2c57ad3c..1d32f182 100644 --- a/android-sdk/android/src/main/kotlin/io/logto/sdk/android/LogtoClient.kt +++ b/android-sdk/android/src/main/kotlin/io/logto/sdk/android/LogtoClient.kt @@ -15,12 +15,14 @@ import io.logto.sdk.android.type.LogtoConfig import io.logto.sdk.android.util.LogtoUtils.expiresAtFrom import io.logto.sdk.android.util.LogtoUtils.nowRoundToSec import io.logto.sdk.core.Core +import io.logto.sdk.core.constant.UserScope import io.logto.sdk.core.type.IdTokenClaims import io.logto.sdk.core.type.OidcConfigResponse import io.logto.sdk.core.type.UserInfoResponse import io.logto.sdk.core.util.TokenUtils import org.jetbrains.annotations.TestOnly import org.jose4j.jwk.JsonWebKeySet +import org.jose4j.jwt.JwtClaims import org.jose4j.jwt.consumer.InvalidJwtException import org.jose4j.lang.JoseException @@ -118,6 +120,10 @@ open class LogtoClient( issuer = oidcConfig.issuer, responseIdToken = codeToken.idToken, responseRefreshToken = codeToken.refreshToken, + /** + * Treat `scopes` as `null` to construct the default access token key + */ + accessTokenKey = buildAccessTokenKey(), accessToken = accessToken, completion = completion, ) @@ -176,7 +182,28 @@ open class LogtoClient( * @param[completion] the completion which handles the result */ fun getAccessToken(completion: Completion) = - getAccessToken(null, completion) + getAccessToken(null, null, completion) + + /** + * Get the access token for the specified organization with refresh strategy. + * + * Scope `UserScope.Organizations` is required in the config to use organization-related methods. + * + */ + fun getOrganizationToken( + organizationId: String, + completion: Completion, + ) { + if (!logtoConfig.scopes.contains(UserScope.ORGANIZATIONS)) { + completion.onComplete( + LogtoException(LogtoException.Type.MISSING_SCOPE_ORGANIZATIONS), + null, + ) + return + } + + return getAccessToken(null, organizationId, completion) + } /** * Get access token @@ -185,6 +212,7 @@ open class LogtoClient( */ fun getAccessToken( resource: String?, + organizationId: String?, completion: Completion, ) { if (!isAuthenticated) { @@ -203,7 +231,7 @@ open class LogtoClient( } // MARK: Retrieve access token from accessTokenMap - val accessTokenKey = buildAccessTokenKey(null, resource) + val accessTokenKey = buildAccessTokenKey(null, resource, organizationId) val accessToken = accessTokenMap[accessTokenKey] accessToken?.let { if (it.expiresAt > nowRoundToSec()) { @@ -230,6 +258,7 @@ open class LogtoClient( clientId = logtoConfig.appId, refreshToken = requireNotNull(refreshToken), resource = resource, + organizationId = organizationId, scopes = null, ) { fetchRefreshedTokenException, fetchedTokenResponse -> fetchRefreshedTokenException?.let { @@ -257,6 +286,7 @@ open class LogtoClient( issuer = oidcConfig.issuer, responseIdToken = refreshedToken.idToken, responseRefreshToken = refreshedToken.refreshToken, + accessTokenKey = buildAccessTokenKey(null, resource, organizationId), accessToken = refreshedAccessToken, ) { verifyException -> verifyException?.let { completion.onComplete(it, null) } @@ -286,6 +316,34 @@ open class LogtoClient( } } + /** + * Get the organization token claims for the specified organization. + * + * @param[organizationId] The ID of the organization that the access token is granted for. + * @param[completion] the completion which handles the retrieved result + */ + fun getOrganizationTokenClaims( + organizationId: String, + completion: Completion, + ) { + getOrganizationToken(organizationId) { getOrgTokenException, token -> + getOrgTokenException?.let { + completion.onComplete(it, null) + return@getOrganizationToken + } + + try { + val tokenClaims = TokenUtils.decodeToken(requireNotNull(token).token) + completion.onComplete(null, tokenClaims) + } catch (exception: InvalidJwtException) { + completion.onComplete( + LogtoException(LogtoException.Type.UNABLE_TO_PARSE_TOKEN_CLAIMS, exception), + null, + ) + } + } + } + /** * Fetch user info * @param[completion] the completion which handles the retrieved result @@ -322,6 +380,7 @@ open class LogtoClient( issuer: String, responseIdToken: String?, responseRefreshToken: String?, + accessTokenKey: String, accessToken: AccessToken, completion: EmptyCompletion, ) { @@ -340,10 +399,6 @@ open class LogtoClient( idToken = it } - // Note - // - Treat `scopes` as `null` to construct the default access token key - // for we do not support custom scopes in V1 - val accessTokenKey = buildAccessTokenKey(null, getResourceFromAccessToken(accessToken.token)) accessTokenMap[accessTokenKey] = accessToken refreshToken = responseRefreshToken completion.onComplete(null) @@ -405,16 +460,15 @@ open class LogtoClient( idToken = storage?.getItem(StorageKey.ID_TOKEN) } - private fun getResourceFromAccessToken(accessToken: String) = try { - TokenUtils.decodeToken(accessToken).audience[0] - } catch (_: InvalidJwtException) { - null - } - - internal fun buildAccessTokenKey(scopes: List?, resource: String?): String { + internal fun buildAccessTokenKey( + scopes: List? = null, + resource: String? = null, + organizationId: String? = null, + ): String { val scopesPart = scopes?.sorted()?.joinToString(" ") ?: "" val resourcePart = resource ?: "" - return "$scopesPart@$resourcePart" + val organizationPart = organizationId?.let { "#$it" } ?: "" + return "$scopesPart@$resourcePart$organizationPart" } @TestOnly diff --git a/android-sdk/android/src/main/kotlin/io/logto/sdk/android/exception/LogtoException.kt b/android-sdk/android/src/main/kotlin/io/logto/sdk/android/exception/LogtoException.kt index db028046..8213a1b4 100644 --- a/android-sdk/android/src/main/kotlin/io/logto/sdk/android/exception/LogtoException.kt +++ b/android-sdk/android/src/main/kotlin/io/logto/sdk/android/exception/LogtoException.kt @@ -15,11 +15,13 @@ class LogtoException( UNABLE_TO_FETCH_TOKEN_BY_AUTHORIZATION_CODE, UNABLE_TO_FETCH_TOKEN_BY_REFRESH_TOKEN, UNABLE_TO_REVOKE_TOKEN, + UNABLE_TO_PARSE_TOKEN_CLAIMS, UNABLE_TO_PARSE_ID_TOKEN_CLAIMS, UNABLE_TO_FETCH_USER_INFO, UNABLE_TO_FETCH_JWKS_JSON, UNABLE_TO_PARSE_JWKS, INVALID_ID_TOKEN, + MISSING_SCOPE_ORGANIZATIONS, ALIPAY_APP_ID_NO_FOUND, ALIPAY_AUTH_FAILED, WECHAT_APP_ID_NO_FOUND, diff --git a/android-sdk/android/src/main/kotlin/io/logto/sdk/android/type/LogtoConfig.kt b/android-sdk/android/src/main/kotlin/io/logto/sdk/android/type/LogtoConfig.kt index 4b408734..c05a7385 100644 --- a/android-sdk/android/src/main/kotlin/io/logto/sdk/android/type/LogtoConfig.kt +++ b/android-sdk/android/src/main/kotlin/io/logto/sdk/android/type/LogtoConfig.kt @@ -1,15 +1,28 @@ package io.logto.sdk.android.type import io.logto.sdk.core.constant.PromptValue +import io.logto.sdk.core.constant.ReservedResource +import io.logto.sdk.core.constant.UserScope import io.logto.sdk.core.util.ScopeUtils class LogtoConfig( val endpoint: String, val appId: String, scopes: List? = null, - val resources: List? = null, + resources: List? = null, val usingPersistStorage: Boolean = true, val prompt: String = PromptValue.CONSENT, ) { + /** + * Normalize the Logto client configuration per the following rules: + * + * - Add default scopes (`openid`, `offline_access` and `profile`) if not provided. + * - Add `ReservedResource.Organization` to resources if `UserScope.Organizations` is included in scopes. + */ val scopes = ScopeUtils.withDefaultScopes(scopes) + val resources = if (this.scopes.contains(UserScope.ORGANIZATIONS)) { + (resources.orEmpty() + ReservedResource.ORGANIZATION) + } else { + resources + } } diff --git a/android-sdk/android/src/test/kotlin/io/logto/sdk/android/LogtoClientTest.kt b/android-sdk/android/src/test/kotlin/io/logto/sdk/android/LogtoClientTest.kt index 48d3dd41..34a8c4e6 100644 --- a/android-sdk/android/src/test/kotlin/io/logto/sdk/android/LogtoClientTest.kt +++ b/android-sdk/android/src/test/kotlin/io/logto/sdk/android/LogtoClientTest.kt @@ -270,6 +270,7 @@ class LogtoClientTest { logtoClient.getAccessToken( resource = TEST_RESOURCE_3, + organizationId = null, ) { logtoException, result -> assertThat(logtoException) .hasMessageThat() @@ -284,7 +285,7 @@ class LogtoClientTest { logtoClient = LogtoClient(logtoConfigMock, mockk()) logtoClient.setupRefreshToken(TEST_REFRESH_TOKEN) - val testTokenKey = logtoClient.buildAccessTokenKey(null, null) + val testTokenKey = logtoClient.buildAccessTokenKey() val testAccessToken: AccessToken = mockk() every { testAccessToken.expiresAt } returns LogtoUtils.nowRoundToSec() + timeBias @@ -295,6 +296,7 @@ class LogtoClientTest { logtoClient.getAccessToken( null, + null, ) { logtoException, result -> assertThat(logtoException).isNull() assertThat(result).isEqualTo(testAccessToken) @@ -305,13 +307,14 @@ class LogtoClientTest { fun `getAccessToken should refresh token when existing accessToken is expired`() { setupRefreshTokenTestEnv() - val expiredAccessTokenKey = logtoClient.buildAccessTokenKey(null, null) + val expiredAccessTokenKey = logtoClient.buildAccessTokenKey() val expiredAccessToken: AccessToken = mockk() every { expiredAccessToken.expiresAt } returns LogtoUtils.nowRoundToSec() - timeBias logtoClient.setupAccessTokenMap(mapOf(expiredAccessTokenKey to expiredAccessToken)) logtoClient.getAccessToken( null, + null, ) { logtoException, result -> assertThat(logtoException).isNull() assertThat(result).isNotNull() @@ -321,7 +324,7 @@ class LogtoClientTest { } verify(exactly = 1) { - Core.fetchTokenByRefreshToken(any(), any(), any(), any(), any(), any()) + Core.fetchTokenByRefreshToken(any(), any(), any(), any(), any(), any(), any()) } } @@ -331,6 +334,7 @@ class LogtoClientTest { logtoClient.getAccessToken( null, + null, ) { logtoException, result -> assertThat(logtoException).isNull() assertThat(result).isNotNull() @@ -340,7 +344,7 @@ class LogtoClientTest { } verify(exactly = 1) { - Core.fetchTokenByRefreshToken(any(), any(), any(), any(), any(), any()) + Core.fetchTokenByRefreshToken(any(), any(), any(), any(), any(), any(), any()) } } @@ -453,7 +457,7 @@ class LogtoClientTest { } val accessTokenMock: AccessToken = mockk() every { accessTokenMock.token } returns TEST_ACCESS_TOKEN - every { logtoClient.getAccessToken(any(), any()) } answers { + every { logtoClient.getAccessToken(any(), any(), any()) } answers { lastArg>().onComplete(null, accessTokenMock) } @@ -502,7 +506,7 @@ class LogtoClientTest { } val mockGetAccessTokenException: LogtoException = mockk() - every { logtoClient.getAccessToken(any(), any()) } answers { + every { logtoClient.getAccessToken(any(), any(), any()) } answers { lastArg>().onComplete(mockGetAccessTokenException, null) } @@ -524,7 +528,7 @@ class LogtoClientTest { } val accessTokenMock: AccessToken = mockk() every { accessTokenMock.token } returns TEST_ACCESS_TOKEN - every { logtoClient.getAccessToken(any(), any()) } answers { + every { logtoClient.getAccessToken(any(), any(), any()) } answers { lastArg>().onComplete(null, accessTokenMock) } @@ -701,7 +705,7 @@ class LogtoClientTest { every { refreshTokenTokenResponseMock.idToken } returns TEST_ID_TOKEN mockkObject(Core) - every { Core.fetchTokenByRefreshToken(any(), any(), any(), any(), any(), any()) } answers { + every { Core.fetchTokenByRefreshToken(any(), any(), any(), any(), any(), any(), any()) } answers { lastArg>() .onComplete(null, refreshTokenTokenResponseMock) } diff --git a/android-sdk/android/src/test/kotlin/io/logto/sdk/android/type/LogtoConfigTest.kt b/android-sdk/android/src/test/kotlin/io/logto/sdk/android/type/LogtoConfigTest.kt index 6182112e..7e9acc4e 100644 --- a/android-sdk/android/src/test/kotlin/io/logto/sdk/android/type/LogtoConfigTest.kt +++ b/android-sdk/android/src/test/kotlin/io/logto/sdk/android/type/LogtoConfigTest.kt @@ -1,6 +1,7 @@ package io.logto.sdk.android.type import com.google.common.truth.Truth.assertThat +import io.logto.sdk.core.constant.ReservedResource import io.logto.sdk.core.constant.ReservedScope import io.logto.sdk.core.constant.UserScope import org.junit.Test @@ -32,4 +33,15 @@ class LogtoConfigTest { contains("other_scope") } } + + @Test + fun `LogtoConfig's resource should contain 'organization' if organization scope is provided`() { + val logtoConfig = LogtoConfig( + endpoint = "endpoint", + appId = "appId", + scopes = listOf(UserScope.ORGANIZATIONS) + ) + + assertThat(logtoConfig.resources).contains(ReservedResource.ORGANIZATION) + } } diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/Core.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/Core.kt index a5e0b49c..b9d2ab7c 100644 --- a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/Core.kt +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/Core.kt @@ -88,10 +88,20 @@ object Core { } fun fetchTokenByRefreshToken( + /** The token endpoint of the authorization server. */ tokenEndpoint: String, + /** The client ID of the application. */ clientId: String, + /** The refresh token to be used to fetch the access token. */ refreshToken: String, + /** The API resource to be fetch the access token for. */ resource: String?, + /** The ID of the organization to be fetch the access token for. */ + organizationId: String?, + /** + * The scopes to request for the access token. If not provided, the authorization server + * will use all the scopes that the client is authorized for. + */ scopes: List?, completion: HttpCompletion, ) { @@ -100,6 +110,7 @@ object Core { add(QueryKey.REFRESH_TOKEN, refreshToken) add(QueryKey.GRANT_TYPE, GrantType.REFRESH_TOKEN) resource?.let { add(QueryKey.RESOURCE, it) } + organizationId?.let { add(QueryKey.ORGANIZATION_ID, it) } scopes?.let { add(QueryKey.SCOPE, it.joinToString(" ")) } }.build() httpPost(tokenEndpoint, formBody, completion) diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/ClaimName.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/ClaimName.kt index cba82d37..6ad33644 100644 --- a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/ClaimName.kt +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/ClaimName.kt @@ -9,6 +9,9 @@ object ClaimName { const val EMAIL_VERIFIED = "email_verified" const val PHONE_NUMBER = "phone_number" const val PHONE_NUMBER_VERIFIED = "phone_number_verified" + const val ROLES = "roles" + const val ORGANIZATIONS = "organizations" + const val ORGANIZATION_ROLES = "organization_roles" const val CUSTOM_DATA = "custom_data" const val IDENTITIES = "identities" } diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/QueryKey.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/QueryKey.kt index 904825f4..3342faa0 100644 --- a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/QueryKey.kt +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/QueryKey.kt @@ -18,4 +18,5 @@ object QueryKey { const val SCOPE = "scope" const val STATE = "state" const val TOKEN = "token" + const val ORGANIZATION_ID = "organization_id" } diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/ReservedResource.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/ReservedResource.kt new file mode 100644 index 00000000..7ad4f9b4 --- /dev/null +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/ReservedResource.kt @@ -0,0 +1,5 @@ +package io.logto.sdk.core.constant + +object ReservedResource { + const val ORGANIZATION = "urn:logto:resource:organizations" +} diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/UserScope.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/UserScope.kt index 11742090..504b14a8 100644 --- a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/UserScope.kt +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/UserScope.kt @@ -1,9 +1,37 @@ package io.logto.sdk.core.constant object UserScope { + /** + * Scope for basic user info. + */ const val PROFILE = "profile" + /** + * Scope for user email address. + */ const val EMAIL = "email" + /** + * Scope for user phone number. + */ const val PHONE = "phone" + /** + * Scope for user's custom data. + */ const val CUSTOM_DATA = "custom_data" + /** + * Scope for user's social identity details. + */ const val IDENTITIES = "identities" + /** + * Scope for user's roles. + */ + const val ROLES = "roles" + /** + * Scope for user's organization IDs and perform organization token + * grant per [RFC 0001](https://github.com/logto-io/rfcs). + */ + const val ORGANIZATIONS = "urn:logto:scope:organizations" + /** + * Scope for user's organization roles per [RFC 0001](https://github.com/logto-io/rfcs). + */ + const val ORGANIZATION_ROLES = "urn:logto:scope:organization_roles" } diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/extension/JwtClaimsExt.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/extension/JwtClaimsExt.kt index 8941a6d4..6ff8f17a 100644 --- a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/extension/JwtClaimsExt.kt +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/extension/JwtClaimsExt.kt @@ -18,4 +18,7 @@ fun JwtClaims.toIdTokenClaims(): IdTokenClaims = IdTokenClaims( emailVerified = this.getClaimValue(ClaimName.EMAIL_VERIFIED) as Boolean?, phoneNumber = this.getClaimValueAsString(ClaimName.PHONE_NUMBER), phoneNumberVerified = this.getClaimValue(ClaimName.PHONE_NUMBER_VERIFIED) as Boolean?, + roles = this.getStringListClaimValue(ClaimName.ROLES) as List?, + organizations = this.getStringListClaimValue(ClaimName.ORGANIZATIONS) as List?, + organizationRoles = this.getStringListClaimValue(ClaimName.ORGANIZATION_ROLES) as List?, ) diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/type/IdTokenClaims.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/type/IdTokenClaims.kt index 9f45d1ac..503f4165 100644 --- a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/type/IdTokenClaims.kt +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/type/IdTokenClaims.kt @@ -1,23 +1,56 @@ package io.logto.sdk.core.type data class IdTokenClaims( + /** Issuer of this token. */ val iss: String, + /** Subject (the user ID) of this token. */ val sub: String, + /** Audience (the client ID) of this token. */ val aud: String, + /** Expiration time of this token. */ val exp: Long, + /** Time at which this token was issued. */ val iat: Long, val atHash: String?, // Scope `profile` + /** Full name of the user. */ val name: String?, + /** Username of the user. */ val username: String?, + /** URL of the user's profile picture. */ val picture: String?, // Scope `email` + /** Email address of the user. */ val email: String?, + /** Whether the user's email address has been verified. */ val emailVerified: Boolean?, // Scope `phone` + /** Phone number of the user. */ val phoneNumber: String?, + /** Whether the user's phone number has been verified. */ val phoneNumberVerified: Boolean?, + + // Scope `roles` + /** Roles that the user has for API resources. */ + val roles: List?, + + // Scope `urn:logto:scope:organizations` + /** Organization IDs that the user has membership in. */ + val organizations: List?, + + // Scope `urn:logto:scope:organization_roles` + /** + * All organization roles that the user has. The format is `{organizationId}:{roleName}`. + * + * Note that not all organizations are included in this list, only the ones that the user has roles in. + * + * @example + * ```ts + * ['org1:admin', 'org2:member'] // The user is an admin of org1 and a member of org2. + * ``` + */ + val organizationRoles: List?, ) diff --git a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/CoreFetchTest.kt b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/CoreFetchTest.kt index 4faaf691..817128be 100644 --- a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/CoreFetchTest.kt +++ b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/CoreFetchTest.kt @@ -260,7 +260,8 @@ class CoreFetchTest { clientId = "clientId", refreshToken = "refreshToken", resource = null, - scopes = null + scopes = null, + organizationId = null, ) { throwable, response -> throwableReceiver = throwable responseReceiver = response @@ -284,6 +285,7 @@ class CoreFetchTest { refreshToken = "refreshToken", resource = "resource", scopes = listOf("scope1", "scope2"), + organizationId = null, ) { throwable, response -> throwableReceiver = throwable responseReceiver = response diff --git a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/constant/ConstantTest.kt b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/constant/ConstantTest.kt index 5217b9b2..2471704c 100644 --- a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/constant/ConstantTest.kt +++ b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/constant/ConstantTest.kt @@ -12,6 +12,7 @@ class ConstantTest { assertThat(PromptValue).isNotNull() assertThat(QueryKey).isNotNull() assertThat(ReservedScope).isNotNull() + assertThat(ReservedResource).isNotNull() assertThat(UserScope).isNotNull() assertThat(ResponseType).isNotNull() } diff --git a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/extension/JwtClaimsExtKtTest.kt b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/extension/JwtClaimsExtKtTest.kt index fa235082..330a3299 100644 --- a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/extension/JwtClaimsExtKtTest.kt +++ b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/extension/JwtClaimsExtKtTest.kt @@ -23,6 +23,9 @@ class JwtClaimsExtKtTest { val testEmailVerified = true val testPhone = "testPhone" val testPhoneVerified = true + val testRoles = listOf("reader", "writer") + val testOrganizations = listOf("silverhand", "logto") + val testOrganizationRoles = listOf("designer", "engineer") val idTokenClaims = IdTokenClaims( iss = testIssuer, @@ -38,6 +41,9 @@ class JwtClaimsExtKtTest { emailVerified = testEmailVerified, phoneNumber = testPhone, phoneNumberVerified = testPhoneVerified, + roles = testRoles, + organizations = testOrganizations, + organizationRoles = testOrganizationRoles, ) val jwtClaims = JwtClaims().apply { @@ -54,6 +60,9 @@ class JwtClaimsExtKtTest { setClaim(ClaimName.EMAIL_VERIFIED, testEmailVerified) setClaim(ClaimName.PHONE_NUMBER, testPhone) setClaim(ClaimName.PHONE_NUMBER_VERIFIED, testPhoneVerified) + setClaim(ClaimName.ROLES, testRoles) + setClaim(ClaimName.ORGANIZATIONS, testOrganizations) + setClaim(ClaimName.ORGANIZATION_ROLES, testOrganizationRoles) } assertThat(jwtClaims.toIdTokenClaims()).isEqualTo(idTokenClaims) diff --git a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/type/IdTokenClaimsTest.kt b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/type/IdTokenClaimsTest.kt index 80f7cf70..ec82aad7 100644 --- a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/type/IdTokenClaimsTest.kt +++ b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/type/IdTokenClaimsTest.kt @@ -18,6 +18,9 @@ class IdTokenClaimsTest { private val emailVerified = true private val phoneNumber = "123456789" private val phoneNumberVerified = true + private val roles = listOf("reader", "writer") + private val organizations = listOf("silverhand", "logto") + private val organizationRoles = listOf("designer", "engineer") private val idTokenClaims = IdTokenClaims( iss = iss, @@ -33,6 +36,9 @@ class IdTokenClaimsTest { emailVerified = emailVerified, phoneNumber = phoneNumber, phoneNumberVerified = phoneNumberVerified, + roles = roles, + organizations = organizations, + organizationRoles = organizationRoles, ) @Test @@ -51,5 +57,8 @@ class IdTokenClaimsTest { assertThat(idTokenClaims.emailVerified).isEqualTo(emailVerified) assertThat(idTokenClaims.phoneNumber).isEqualTo(phoneNumber) assertThat(idTokenClaims.phoneNumberVerified).isEqualTo(phoneNumberVerified) + assertThat(idTokenClaims.roles).isEqualTo(roles) + assertThat(idTokenClaims.organizations).isEqualTo(organizations) + assertThat(idTokenClaims.organizationRoles).isEqualTo(organizationRoles) } }