From 71816a415fc18be20e23f98af94ac97be5c296e9 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 5 Sep 2024 13:39:40 +0800 Subject: [PATCH] feat(core): add more options for sign-in uri generation --- .../src/main/kotlin/io/logto/sdk/core/Core.kt | 27 ++++- .../io/logto/sdk/core/constant/FirstScreen.kt | 10 ++ .../io/logto/sdk/core/constant/Identifier.kt | 7 ++ .../io/logto/sdk/core/constant/QueryKey.kt | 4 + .../sdk/core/type/GenerateSignInUriOptions.kt | 5 + .../test/kotlin/io/logto/sdk/core/CoreTest.kt | 113 +++++++++++++++++- 6 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/FirstScreen.kt create mode 100644 kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/Identifier.kt 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 886e5773..77447a17 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 @@ -35,8 +35,15 @@ object Core { addQueryParameter(QueryKey.REDIRECT_URI, options.redirectUri) addQueryParameter(QueryKey.RESPONSE_TYPE, ResponseType.CODE) - val usedScopes = ScopeUtils.withDefaultScopes(options.scopes) - addQueryParameter(QueryKey.SCOPE, usedScopes.joinToString(" ")) + val usedScopes = if (options.includeReservedScopes == true) { + ScopeUtils.withDefaultScopes(options.scopes) + } else { + options.scopes.orEmpty() + } + + if (usedScopes.isNotEmpty()) { + addQueryParameter(QueryKey.SCOPE, usedScopes.joinToString(" ")) + } val usedResources = options.resources.orEmpty() for (value in usedResources) { addQueryParameter(QueryKey.RESOURCE, value) } @@ -48,6 +55,22 @@ object Core { } addQueryParameter(QueryKey.PROMPT, options.prompt ?: PromptValue.CONSENT) + + options.loginHint?.let { + addQueryParameter(QueryKey.LOGIN_HINT, it) + } + + options.firstScreen?.let { + addQueryParameter(QueryKey.FIRST_SCREEN, it) + } + + options.identifiers?.let { + addQueryParameter(QueryKey.IDENTIFIER, it.joinToString(" ")) + } + + options.extraParams?.forEach { (key, value) -> + addQueryParameter(key, value) + } }.build().toString() } diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/FirstScreen.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/FirstScreen.kt new file mode 100644 index 00000000..f148a2a2 --- /dev/null +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/FirstScreen.kt @@ -0,0 +1,10 @@ +package io.logto.sdk.core.constant + +object FirstScreen { + const val SIGN_IN = "sign_in" + const val REGISTER = "register" + const val RESET_PASSWORD = "reset_password" + const val IDENTIFIER_SIGN_IN = "identifier:sign_in" + const val IDENTIFIER_REGISTER = "identifier:register" + const val SINGLE_SIGN_ON = "single_sign_on" +} diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/Identifier.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/Identifier.kt new file mode 100644 index 00000000..5c7feddc --- /dev/null +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/constant/Identifier.kt @@ -0,0 +1,7 @@ +package io.logto.sdk.core.constant + +object Identifier { + const val USERNAME = "username" + const val EMAIL = "email" + const val PHONE = "phone" +} 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 3342faa0..f38a8902 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 @@ -19,4 +19,8 @@ object QueryKey { const val STATE = "state" const val TOKEN = "token" const val ORGANIZATION_ID = "organization_id" + const val LOGIN_HINT = "login_hint" + const val FIRST_SCREEN = "first_screen" + const val IDENTIFIER = "identifier" + const val DIRECT_SIGN_IN = "direct_sign_in" } diff --git a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/type/GenerateSignInUriOptions.kt b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/type/GenerateSignInUriOptions.kt index 2b4b190b..7bca3f9a 100644 --- a/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/type/GenerateSignInUriOptions.kt +++ b/kotlin-sdk/kotlin/src/main/kotlin/io/logto/sdk/core/type/GenerateSignInUriOptions.kt @@ -9,4 +9,9 @@ class GenerateSignInUriOptions( val scopes: List? = null, val resources: List? = null, val prompt: String? = null, + val loginHint: String? = null, + val firstScreen: String? = null, + val identifiers: List? = null, + val extraParams: Map? = null, + val includeReservedScopes: Boolean? = true, ) diff --git a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/CoreTest.kt b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/CoreTest.kt index 90e03a66..9afacb9d 100644 --- a/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/CoreTest.kt +++ b/kotlin-sdk/kotlin/src/test/kotlin/io/logto/sdk/core/CoreTest.kt @@ -189,7 +189,6 @@ class CoreTest { @Test fun `generateSignInUri should always contain reserved scopes if only the reserved OPENID is provided`() { - val signInUri = Core.generateSignInUri( GenerateSignInUriOptions( authorizationEndpoint = testAuthorizationEndpoint, @@ -214,7 +213,6 @@ class CoreTest { @Test fun `generateSignInUri should always contain reserved scopes if only the reserved PROFILE is provided`() { - val signInUri = Core.generateSignInUri( GenerateSignInUriOptions( authorizationEndpoint = testAuthorizationEndpoint, @@ -237,13 +235,117 @@ class CoreTest { } } + @Test + fun `generateSignInUri should not contain reserved scopes if includeReserved options is false`() { + val signInUri = Core.generateSignInUri( + GenerateSignInUriOptions( + authorizationEndpoint = testAuthorizationEndpoint, + clientId = testClientId, + redirectUri = testRedirectUri, + codeChallenge = testCodeChallenge, + state = testState, + scopes = listOf(UserScope.CUSTOM_DATA), + includeReservedScopes = false, + ), + ) + + signInUri.toHttpUrl().apply { + assertThat(queryParameter(QueryKey.SCOPE)).isEqualTo(UserScope.CUSTOM_DATA) + } + } + + @Test + fun `generateSignInUri should contain login_hint if provided`() { + val loginHint = "abc@logto.io" + + val signInUri = Core.generateSignInUri( + GenerateSignInUriOptions( + authorizationEndpoint = testAuthorizationEndpoint, + clientId = testClientId, + redirectUri = testRedirectUri, + codeChallenge = testCodeChallenge, + state = testState, + loginHint = loginHint, + ), + ) + + signInUri.toHttpUrl().apply { + assertThat(queryParameter(QueryKey.LOGIN_HINT)).isEqualTo(loginHint) + } + } + + @Test + fun `generateSignInUri should contain first_screen if provided`() { + val firstScreen = FirstScreen.SIGN_IN + + val signInUri = Core.generateSignInUri( + GenerateSignInUriOptions( + authorizationEndpoint = testAuthorizationEndpoint, + clientId = testClientId, + redirectUri = testRedirectUri, + codeChallenge = testCodeChallenge, + state = testState, + firstScreen = firstScreen, + ) + ) + + signInUri.toHttpUrl().apply { + assertThat(queryParameter(QueryKey.FIRST_SCREEN)).contains(firstScreen) + } + } + + @Test + fun `generateSignInUri should contain identifier if provided`() { + val identifiers = listOf(Identifier.EMAIL, Identifier.PHONE); + val signInUri = Core.generateSignInUri( + GenerateSignInUriOptions( + authorizationEndpoint = testAuthorizationEndpoint, + clientId = testClientId, + redirectUri = testRedirectUri, + codeChallenge = testCodeChallenge, + state = testState, + identifiers = identifiers, + ) + ) + + signInUri.toHttpUrl().apply { + assertThat(queryParameterValues(QueryKey.IDENTIFIER)).contains( + identifiers.joinToString( + " " + ) + ) + } + } + + @Test + fun `generateSignInUri should contain extra params if provided`() { + val extraParamKey = "tenant_id" + val extraParamValue = "abced" + + val signInUri = Core.generateSignInUri( + GenerateSignInUriOptions( + authorizationEndpoint = testAuthorizationEndpoint, + clientId = testClientId, + redirectUri = testRedirectUri, + codeChallenge = testCodeChallenge, + state = testState, + extraParams = mapOf(extraParamKey to extraParamValue), + ) + ) + + signInUri.toHttpUrl().apply { + assertThat(queryParameter(extraParamKey)).contains(extraParamValue) + } + } + @Test fun `generateSignOutUri should contain expected queries`() { val endSessionEndpoint = "https://logto.dev/oidc/endSession" val clientId = "clientId" val postLogoutRedirectUri = "https://myapp.com/logout_callback" - val resultUri = Core.generateSignOutUri(endSessionEndpoint, clientId, postLogoutRedirectUri) + val resultUri = + Core.generateSignOutUri(endSessionEndpoint, clientId, postLogoutRedirectUri) val constructedUri = resultUri.toHttpUrl() assertThat(constructedUri.scheme).isEqualTo(endSessionEndpoint.toHttpUrl().scheme) @@ -267,7 +369,9 @@ class CoreTest { assertThat(constructedUri.host).isEqualTo(endSessionEndpoint.toHttpUrl().host) assertThat(constructedUri.pathSegments).isEqualTo(endSessionEndpoint.toHttpUrl().pathSegments) assertThat(constructedUri.queryParameter(QueryKey.CLIENT_ID)).isEqualTo(clientId) - assertThat(constructedUri.queryParameter(QueryKey.POST_LOGOUT_REDIRECT_URI)).isEqualTo(null) + assertThat(constructedUri.queryParameter(QueryKey.POST_LOGOUT_REDIRECT_URI)).isEqualTo( + null + ) } @Test @@ -283,3 +387,4 @@ class CoreTest { .contains(UriConstructionException.Type.INVALID_ENDPOINT.name) } } +