Skip to content

Commit cb189a2

Browse files
authored
Add support for custom token exchange (#789)
2 parents 6735a79 + feb4b4d commit cb189a2

File tree

6 files changed

+144
-16
lines changed

6 files changed

+144
-16
lines changed

.github/actions/setup/action.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ runs:
1919
using: composite
2020

2121
steps:
22+
- name: Set up Java
23+
uses: actions/setup-java@v4
24+
with:
25+
distribution: 'temurin'
26+
java-version: '11'
27+
2228
- run: |
2329
curl -s "https://get.sdkman.io" | bash
2430
source "/home/runner/.sdkman/bin/sdkman-init.sh"
25-
sdk list java
26-
sdk install java ${{ inputs.java }} && sdk default java ${{ inputs.java }}
2731
sdk install gradle ${{ inputs.gradle }} && sdk default gradle ${{ inputs.gradle }}
2832
sdk install kotlin ${{ inputs.kotlin }} && sdk default kotlin ${{ inputs.kotlin }}
2933
shell: bash

.github/workflows/codeql.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ jobs:
3535
- if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group'
3636
run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection.
3737

38+
- name: Set up Java
39+
uses: actions/setup-java@v4
40+
with:
41+
distribution: 'temurin'
42+
java-version: '11'
43+
3844
- name: Checkout
3945
uses: actions/checkout@v4
4046

.snyk

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ ignore:
55
SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135:
66
- '*':
77
reason: Latest version of dokka has this vulnerability
8-
expires: 2024-12-31T12:54:23.000Z
8+
expires: 2025-02-21T12:54:23.000Z
99
created: 2024-08-01T12:08:37.770Z
1010
SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744:
1111
- '*':
1212
reason: Latest version of dokka has this vulnerability
13-
expires: 2024-12-31T12:54:23.000Z
13+
expires: 2025-02-21T12:54:23.000Z
1414
created: 2024-08-01T12:08:55.927Z
1515
SNYK-JAVA-COMFASTERXMLJACKSONCORE-7569538:
1616
- '*':
1717
reason: Latest version of dokka has this vulnerability
18-
expires: 2024-12-31T1:54:23.000Z
18+
expires: 2025-02-21T1:54:23.000Z
1919
created: 2024-08-01T12:08:02.973Z
2020
patch: {}

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

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
167167
* ```
168168
* client.signinWithPasskey("{authSession}", "{authResponse}","{realm}")
169169
* .validateClaims() //mandatory
170-
* .addParameter("scope","scope")
170+
* .setScope("{scope}")
171171
* .start(object: Callback<Credentials, AuthenticationException> {
172172
* override fun onFailure(error: AuthenticationException) { }
173173
* override fun onSuccess(result: Credentials) { }
@@ -211,7 +211,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
211211
* ```
212212
* client.signinWithPasskey("{authSession}", "{authResponse}","{realm}")
213213
* .validateClaims() //mandatory
214-
* .addParameter("scope","scope")
214+
* .setScope("{scope}")
215215
* .start(object: Callback<Credentials, AuthenticationException> {
216216
* override fun onFailure(error: AuthenticationException) { }
217217
* override fun onSuccess(result: Credentials) { }
@@ -457,13 +457,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
457457
* @return a request to configure and start that will yield [Credentials]
458458
*/
459459
public fun loginWithNativeSocialToken(token: String, tokenType: String): AuthenticationRequest {
460-
val parameters = ParameterBuilder.newAuthenticationBuilder()
461-
.setGrantType(ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
462-
.setClientId(clientId)
463-
.set(SUBJECT_TOKEN_KEY, token)
464-
.set(SUBJECT_TOKEN_TYPE_KEY, tokenType)
465-
.asDictionary()
466-
return loginWithToken(parameters)
460+
return tokenExchange(tokenType, token)
467461
}
468462

469463
/**
@@ -707,6 +701,34 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
707701
.addParameters(parameters)
708702
}
709703

704+
/**
705+
* The Custom Token Exchange feature allows clients to exchange their existing tokens for Auth0 tokens by calling the `/oauth/token` endpoint with specific parameters.
706+
* The default scope used is 'openid profile email'.
707+
*
708+
* Example usage:
709+
*
710+
* ```
711+
* client.customTokenExchange("{subject token type}", "{subject token}")
712+
* .validateClaims() //mandatory
713+
* .setScope("{scope}")
714+
* .setAudience("{audience}")
715+
* .start(object: Callback<Credentials, AuthenticationException> {
716+
* override fun onSuccess(result: Credentials) { }
717+
* override fun onFailure(error: AuthenticationException) { }
718+
* })
719+
* ```
720+
*
721+
* @param subjectTokenType the subject token type that is associated with the existing Identity Provider. e.g. 'http://acme.com/legacy-token'
722+
* @param subjectToken the subject token, typically obtained through the Identity Provider's SDK
723+
* @return a request to configure and start that will yield [Credentials]
724+
*/
725+
public fun customTokenExchange(
726+
subjectTokenType: String,
727+
subjectToken: String,
728+
): AuthenticationRequest {
729+
return tokenExchange(subjectTokenType, subjectToken)
730+
}
731+
710732
/**
711733
* Requests new Credentials using a valid Refresh Token. The received token will have the same audience and scope as first requested.
712734
*
@@ -922,6 +944,21 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
922944
return request
923945
}
924946

947+
/**
948+
* Helper function to make a request to the /oauth/token endpoint with the token exchange grant type.
949+
*/
950+
private fun tokenExchange(
951+
subjectTokenType: String,
952+
subjectToken: String
953+
): AuthenticationRequest {
954+
val parameters = ParameterBuilder.newAuthenticationBuilder()
955+
.setGrantType(ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
956+
.set(SUBJECT_TOKEN_TYPE_KEY, subjectTokenType)
957+
.set(SUBJECT_TOKEN_KEY, subjectToken)
958+
.asDictionary()
959+
return loginWithToken(parameters)
960+
}
961+
925962
private fun profileRequest(): Request<UserProfile, AuthenticationException> {
926963
val url =
927964
auth0.getDomainUrl().toHttpUrl().newBuilder()

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,6 +2272,87 @@ public class AuthenticationAPIClientTest {
22722272
assertThat(body, Matchers.hasEntry("token", "refreshToken"))
22732273
}
22742274

2275+
@Test
2276+
public fun shouldCustomTokenExchange() {
2277+
mockAPI.willReturnSuccessfulLogin()
2278+
val callback = MockAuthenticationCallback<Credentials>()
2279+
client.customTokenExchange( "subject-token-type","subject-token")
2280+
.start(callback)
2281+
ShadowLooper.idleMainLooper()
2282+
val request = mockAPI.takeRequest()
2283+
assertThat(
2284+
request.getHeader("Accept-Language"), Matchers.`is`(
2285+
defaultLocale
2286+
)
2287+
)
2288+
assertThat(request.path, Matchers.equalTo("/oauth/token"))
2289+
val body = bodyFromRequest<String>(request)
2290+
assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID))
2291+
assertThat(
2292+
body,
2293+
Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
2294+
)
2295+
assertThat(body, Matchers.hasEntry("subject_token", "subject-token"))
2296+
assertThat(body, Matchers.hasEntry("subject_token_type", "subject-token-type"))
2297+
assertThat(body, Matchers.hasEntry("scope", "openid profile email"))
2298+
assertThat(
2299+
callback, AuthenticationCallbackMatcher.hasPayloadOfType(
2300+
Credentials::class.java
2301+
)
2302+
)
2303+
}
2304+
2305+
@Test
2306+
public fun shouldCustomTokenExchangeSync() {
2307+
mockAPI.willReturnSuccessfulLogin()
2308+
val credentials = client
2309+
.customTokenExchange("subject-token-type", "subject-token")
2310+
.execute()
2311+
val request = mockAPI.takeRequest()
2312+
assertThat(
2313+
request.getHeader("Accept-Language"), Matchers.`is`(
2314+
defaultLocale
2315+
)
2316+
)
2317+
assertThat(request.path, Matchers.equalTo("/oauth/token"))
2318+
val body = bodyFromRequest<String>(request)
2319+
assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID))
2320+
assertThat(
2321+
body,
2322+
Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
2323+
)
2324+
assertThat(body, Matchers.hasEntry("subject_token", "subject-token"))
2325+
assertThat(body, Matchers.hasEntry("subject_token_type", "subject-token-type"))
2326+
assertThat(body, Matchers.hasEntry("scope", "openid profile email"))
2327+
assertThat(credentials, Matchers.`is`(Matchers.notNullValue()))
2328+
}
2329+
2330+
@Test
2331+
@ExperimentalCoroutinesApi
2332+
public fun shouldAwaitCustomTokenExchnage(): Unit = runTest {
2333+
mockAPI.willReturnSuccessfulLogin()
2334+
val credentials = client
2335+
.customTokenExchange("subject-token-type", "subject-token")
2336+
.await()
2337+
val request = mockAPI.takeRequest()
2338+
assertThat(
2339+
request.getHeader("Accept-Language"), Matchers.`is`(
2340+
defaultLocale
2341+
)
2342+
)
2343+
assertThat(request.path, Matchers.equalTo("/oauth/token"))
2344+
val body = bodyFromRequest<String>(request)
2345+
assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID))
2346+
assertThat(
2347+
body,
2348+
Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
2349+
)
2350+
assertThat(body, Matchers.hasEntry("subject_token", "subject-token"))
2351+
assertThat(body, Matchers.hasEntry("subject_token_type", "subject-token-type"))
2352+
assertThat(body, Matchers.hasEntry("scope", "openid profile email"))
2353+
assertThat(credentials, Matchers.`is`(Matchers.notNullValue()))
2354+
}
2355+
22752356
@Test
22762357
public fun shouldRenewAuthWithOAuthToken() {
22772358
val auth0 = auth0
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<resources>
22
<string name="app_name">Auth0 SDK Sample</string>
3-
<string name="com_auth0_domain">p-mathew.us.auth0.com</string>
4-
<string name="com_auth0_client_id">gkba7X6OJM2b0cdlUlTCqXD7AwT3FYVV</string>
3+
<string name="com_auth0_domain">YOUR_DOMAIN</string>
4+
<string name="com_auth0_client_id">YOUR_CLIENT_ID</string>
55
<string name="com_auth0_scheme">demo</string>
66
</resources>

0 commit comments

Comments
 (0)