Skip to content

Commit 2f6dc87

Browse files
authored
feat: Add support for organization to custom token exchange (#885)
1 parent 374e190 commit 2f6dc87

File tree

10 files changed

+132
-19
lines changed

10 files changed

+132
-19
lines changed

auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,21 @@ import com.auth0.android.NetworkErrorException
88
import com.auth0.android.dpop.DPoP
99
import com.auth0.android.dpop.DPoPException
1010
import com.auth0.android.dpop.SenderConstraining
11-
import com.auth0.android.request.*
12-
import com.auth0.android.request.internal.*
11+
import com.auth0.android.request.AuthenticationRequest
12+
import com.auth0.android.request.ErrorAdapter
13+
import com.auth0.android.request.JsonAdapter
14+
import com.auth0.android.request.ProfileRequest
15+
import com.auth0.android.request.PublicKeyCredentials
16+
import com.auth0.android.request.Request
17+
import com.auth0.android.request.SignUpRequest
18+
import com.auth0.android.request.UserData
19+
import com.auth0.android.request.internal.BaseAuthenticationRequest
20+
import com.auth0.android.request.internal.BaseRequest
21+
import com.auth0.android.request.internal.GsonAdapter
1322
import com.auth0.android.request.internal.GsonAdapter.Companion.forMap
1423
import com.auth0.android.request.internal.GsonAdapter.Companion.forMapOf
24+
import com.auth0.android.request.internal.GsonProvider
25+
import com.auth0.android.request.internal.RequestFactory
1526
import com.auth0.android.request.internal.ResponseUtils.isNetworkError
1627
import com.auth0.android.result.Challenge
1728
import com.auth0.android.result.Credentials
@@ -749,13 +760,15 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
749760
*
750761
* @param subjectTokenType the subject token type that is associated with the existing Identity Provider. e.g. 'http://acme.com/legacy-token'
751762
* @param subjectToken the subject token, typically obtained through the Identity Provider's SDK
763+
* @param organization id of the organization the user belongs to
752764
* @return a request to configure and start that will yield [Credentials]
753765
*/
754766
public fun customTokenExchange(
755767
subjectTokenType: String,
756768
subjectToken: String,
769+
organization: String? = null
757770
): AuthenticationRequest {
758-
return tokenExchange(subjectTokenType, subjectToken)
771+
return tokenExchange(subjectTokenType, subjectToken, organization)
759772
}
760773

761774
/**
@@ -1043,13 +1056,17 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
10431056
*/
10441057
private fun tokenExchange(
10451058
subjectTokenType: String,
1046-
subjectToken: String
1059+
subjectToken: String,
1060+
organization: String? = null
10471061
): AuthenticationRequest {
1048-
val parameters = ParameterBuilder.newAuthenticationBuilder()
1049-
.setGrantType(ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
1050-
.set(SUBJECT_TOKEN_TYPE_KEY, subjectTokenType)
1051-
.set(SUBJECT_TOKEN_KEY, subjectToken)
1052-
.asDictionary()
1062+
val parameters = ParameterBuilder.newAuthenticationBuilder().apply {
1063+
setGrantType(ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
1064+
set(SUBJECT_TOKEN_TYPE_KEY, subjectTokenType)
1065+
set(SUBJECT_TOKEN_KEY, subjectToken)
1066+
organization?.let {
1067+
set(ORGANIZATION_KEY, it)
1068+
}
1069+
}.asDictionary()
10531070
return loginWithToken(parameters)
10541071
}
10551072

auth0/src/main/java/com/auth0/android/request/AuthenticationRequest.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.auth0.android.request
22

33
import com.auth0.android.Auth0
44
import com.auth0.android.authentication.AuthenticationException
5-
import com.auth0.android.callback.Callback
65
import com.auth0.android.result.Credentials
76

87
/**
@@ -74,4 +73,15 @@ public interface AuthenticationRequest : Request<Credentials, AuthenticationExce
7473
* @return the current builder instance
7574
*/
7675
public fun withIdTokenVerificationIssuer(issuer: String): AuthenticationRequest
76+
77+
/**
78+
* Adds a validator to be executed before the request is sent.
79+
* Multiple validators can be added and will be executed in order.
80+
*
81+
* @param validator the validator to add
82+
* @return itself
83+
*/
84+
override fun addValidator(validator: RequestValidator): AuthenticationRequest {
85+
return this
86+
}
7787
}

auth0/src/main/java/com/auth0/android/request/Request.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,15 @@ public interface Request<T, U : Auth0Exception> {
7676
* @return itself
7777
*/
7878
public fun addHeader(name: String, value: String): Request<T, U>
79+
80+
/**
81+
* Adds a validator to be executed before the request is sent.
82+
* Multiple validators can be added and will be executed in order.
83+
*
84+
* @param validator the validator to add
85+
* @return itself
86+
*/
87+
public fun addValidator(validator: RequestValidator): Request<T, U> {
88+
return this
89+
}
7990
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.auth0.android.request
2+
3+
import com.auth0.android.Auth0Exception
4+
5+
/**
6+
* Interface for validating request parameters before execution.
7+
* Validators are invoked before the network request is made.
8+
*/
9+
public interface RequestValidator {
10+
11+
/**
12+
* Validates the request options and parameters.
13+
* @param options the request options to validate
14+
* @throws Auth0Exception if validation fails
15+
*/
16+
@Throws(Auth0Exception::class)
17+
public fun validate(options: RequestOptions)
18+
}

auth0/src/main/java/com/auth0/android/request/SignUpRequest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ public class SignUpRequest
9191
return this
9292
}
9393

94+
override fun addValidator(validator: RequestValidator): AuthenticationRequest {
95+
authenticationRequest.addValidator(validator)
96+
return this
97+
}
98+
9499
override fun withIdTokenVerificationLeeway(leeway: Int): SignUpRequest {
95100
authenticationRequest.withIdTokenVerificationLeeway(leeway)
96101
return this

auth0/src/main/java/com/auth0/android/request/internal/BaseAuthenticationRequest.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ import com.auth0.android.Auth0Exception
77
import com.auth0.android.authentication.AuthenticationException
88
import com.auth0.android.authentication.ParameterBuilder
99
import com.auth0.android.callback.Callback
10-
import com.auth0.android.provider.*
10+
import com.auth0.android.provider.IdTokenMissingException
11+
import com.auth0.android.provider.IdTokenVerificationOptions
12+
import com.auth0.android.provider.IdTokenVerifier
13+
import com.auth0.android.provider.TokenValidationException
14+
import com.auth0.android.provider.UnexpectedIdTokenException
1115
import com.auth0.android.request.AuthenticationRequest
1216
import com.auth0.android.request.Request
17+
import com.auth0.android.request.RequestValidator
1318
import com.auth0.android.result.Credentials
14-
import java.util.*
19+
import java.util.Date
1520

1621
internal open class BaseAuthenticationRequest(
1722
private val request: Request<Credentials, AuthenticationException>,
@@ -97,6 +102,11 @@ internal open class BaseAuthenticationRequest(
97102
return this
98103
}
99104

105+
override fun addValidator(validator: RequestValidator): AuthenticationRequest {
106+
request.addValidator(validator)
107+
return this
108+
}
109+
100110
override fun validateClaims(): AuthenticationRequest {
101111
this.validateClaims = true
102112
return this

auth0/src/main/java/com/auth0/android/request/internal/BaseRequest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.auth0.android.request.JsonAdapter
1212
import com.auth0.android.request.NetworkingClient
1313
import com.auth0.android.request.Request
1414
import com.auth0.android.request.RequestOptions
15+
import com.auth0.android.request.RequestValidator
1516
import com.auth0.android.request.ServerResponse
1617
import kotlinx.coroutines.CoroutineDispatcher
1718
import kotlinx.coroutines.Dispatchers
@@ -40,6 +41,8 @@ internal open class BaseRequest<T, U : Auth0Exception>(
4041

4142
private val options: RequestOptions = RequestOptions(method)
4243

44+
private val validators = mutableListOf<RequestValidator>()
45+
4346
override fun addHeader(name: String, value: String): Request<T, U> {
4447
options.headers[name] = value
4548
return this
@@ -70,6 +73,11 @@ internal open class BaseRequest<T, U : Auth0Exception>(
7073
return this
7174
}
7275

76+
override fun addValidator(validator: RequestValidator): Request<T, U> {
77+
validators.add(validator)
78+
return this
79+
}
80+
7381
/**
7482
* Runs asynchronously and executes the network request, without blocking the current thread.
7583
* The result is parsed into a <T> value and posted in the callback's onSuccess method or a <U>
@@ -126,6 +134,7 @@ internal open class BaseRequest<T, U : Auth0Exception>(
126134
*/
127135
@kotlin.jvm.Throws(Auth0Exception::class)
128136
override fun execute(): T {
137+
runClientValidation()
129138
val response: ServerResponse
130139
try {
131140
if (dPoP?.shouldGenerateProof(url, options.parameters) == true) {
@@ -176,4 +185,10 @@ internal open class BaseRequest<T, U : Auth0Exception>(
176185
}
177186
}
178187

188+
private fun runClientValidation() {
189+
validators.forEach { validator ->
190+
validator.validate(options)
191+
}
192+
}
193+
179194
}

auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2298,7 +2298,7 @@ public class AuthenticationAPIClientTest {
22982298
public fun shouldCustomTokenExchange() {
22992299
mockAPI.willReturnSuccessfulLogin()
23002300
val callback = MockAuthenticationCallback<Credentials>()
2301-
client.customTokenExchange("subject-token-type", "subject-token")
2301+
client.customTokenExchange("subject-token-type", "subject-token", "org_12345")
23022302
.start(callback)
23032303
ShadowLooper.idleMainLooper()
23042304
val request = mockAPI.takeRequest()
@@ -2316,6 +2316,7 @@ public class AuthenticationAPIClientTest {
23162316
)
23172317
assertThat(body, Matchers.hasEntry("subject_token", "subject-token"))
23182318
assertThat(body, Matchers.hasEntry("subject_token_type", "subject-token-type"))
2319+
assertThat(body, Matchers.hasEntry("organization", "org_12345"))
23192320
assertThat(body, Matchers.hasEntry("scope", "openid profile email"))
23202321
assertThat(
23212322
callback, AuthenticationCallbackMatcher.hasPayloadOfType(
@@ -2328,7 +2329,7 @@ public class AuthenticationAPIClientTest {
23282329
public fun shouldCustomTokenExchangeSync() {
23292330
mockAPI.willReturnSuccessfulLogin()
23302331
val credentials = client
2331-
.customTokenExchange("subject-token-type", "subject-token")
2332+
.customTokenExchange("subject-token-type", "subject-token", "org_abc")
23322333
.execute()
23332334
val request = mockAPI.takeRequest()
23342335
assertThat(
@@ -2345,6 +2346,7 @@ public class AuthenticationAPIClientTest {
23452346
)
23462347
assertThat(body, Matchers.hasEntry("subject_token", "subject-token"))
23472348
assertThat(body, Matchers.hasEntry("subject_token_type", "subject-token-type"))
2349+
assertThat(body, Matchers.hasEntry("organization", "org_abc"))
23482350
assertThat(body, Matchers.hasEntry("scope", "openid profile email"))
23492351
assertThat(credentials, Matchers.`is`(Matchers.notNullValue()))
23502352
}
@@ -2371,6 +2373,7 @@ public class AuthenticationAPIClientTest {
23712373
)
23722374
assertThat(body, Matchers.hasEntry("subject_token", "subject-token"))
23732375
assertThat(body, Matchers.hasEntry("subject_token_type", "subject-token-type"))
2376+
assertThat(body, Matchers.not(Matchers.hasKey("organization")))
23742377
assertThat(body, Matchers.hasEntry("scope", "openid profile email"))
23752378
assertThat(credentials, Matchers.`is`(Matchers.notNullValue()))
23762379
}
@@ -2871,7 +2874,10 @@ public class AuthenticationAPIClientTest {
28712874
assertThat(request.path, Matchers.equalTo("/oauth/token"))
28722875

28732876
val body = bodyFromRequest<String>(request)
2874-
assertThat(body, Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_AUTHORIZATION_CODE))
2877+
assertThat(
2878+
body,
2879+
Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_AUTHORIZATION_CODE)
2880+
)
28752881
assertThat(body, Matchers.hasEntry("code", "auth-code"))
28762882

28772883
// Verify that key pair generation was attempted
@@ -2935,7 +2941,10 @@ public class AuthenticationAPIClientTest {
29352941
assertThat(request.path, Matchers.equalTo("/oauth/token"))
29362942

29372943
val body = bodyFromRequest<String>(request)
2938-
assertThat(body, Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE))
2944+
assertThat(
2945+
body,
2946+
Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
2947+
)
29392948
assertThat(body, Matchers.hasEntry("subject_token_type", "subject-token-type"))
29402949

29412950
// Verify that key pair generation was attempted
@@ -3114,9 +3123,12 @@ public class AuthenticationAPIClientTest {
31143123
client.useDPoP(mockContext).login(SUPPORT_AUTH0_COM, PASSWORD, MY_CONNECTION)
31153124
.execute()
31163125
}
3117-
Assert.assertEquals("Key pair is not found in the keystore. Please generate a key pair first.", exception.message)
3126+
Assert.assertEquals(
3127+
"Key pair is not found in the keystore. Please generate a key pair first.",
3128+
exception.message
3129+
)
31183130
assertThat(exception.cause, Matchers.notNullValue())
3119-
assertThat(exception.cause, Matchers.instanceOf(DPoPException::class.java ))
3131+
assertThat(exception.cause, Matchers.instanceOf(DPoPException::class.java))
31203132
}
31213133

31223134
private fun <T> bodyFromRequest(request: RecordedRequest): Map<String, T> {

auth0/src/test/java/com/auth0/android/authentication/request/AuthenticationRequestMock.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
import com.auth0.android.callback.Callback;
88
import com.auth0.android.request.AuthenticationRequest;
99
import com.auth0.android.request.Request;
10+
import com.auth0.android.request.RequestValidator;
1011
import com.auth0.android.result.Credentials;
1112

13+
import org.jetbrains.annotations.NotNull;
14+
1215
import java.util.Map;
1316

1417
public class AuthenticationRequestMock implements AuthenticationRequest {
@@ -112,4 +115,9 @@ public AuthenticationRequest withIdTokenVerificationLeeway(int leeway) {
112115
public AuthenticationRequest withIdTokenVerificationIssuer(@NonNull String issuer) {
113116
return this;
114117
}
118+
119+
@Override
120+
public @NotNull AuthenticationRequest addValidator(@NotNull RequestValidator validator) {
121+
return this;
122+
}
115123
}

auth0/src/test/java/com/auth0/android/authentication/request/RequestMock.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
import com.auth0.android.Auth0Exception;
66
import com.auth0.android.callback.Callback;
7-
import com.auth0.android.request.HttpMethod;
87
import com.auth0.android.request.Request;
8+
import com.auth0.android.request.RequestValidator;
9+
10+
import org.jetbrains.annotations.NotNull;
911

1012
import java.util.Map;
1113

@@ -62,4 +64,9 @@ public T execute() throws Auth0Exception {
6264
public Request<T, U> addParameter(@NonNull String name, @NonNull Object value) {
6365
return this;
6466
}
67+
68+
@Override
69+
public @NotNull Request<T, @NotNull U> addValidator(@NotNull RequestValidator validator) {
70+
return this;
71+
}
6572
}

0 commit comments

Comments
 (0)