Flow type: Authentication
Platform: Android
Provider: Duende
generic-oauth2 version: 5.0.0
Describe the Feature
The ability to disable the authState.performActionWithFreshTokens() call during the authenticate flow. For cases where the providers invalidate refreshTokens with each tokenRequest regardless of the refreshToken being used.
When working with a provider that rolls new refreshTokens anytime the token endpoints are hit, regardless of using a refreshToken, the Android implementation never gets the 2nd refreshToken created. IOS doesn't appear to have this issue and does not attempt a second token request for 'fresh' tokens during authentication.
The Android implementation of the authenticate method attempts two separate token requests. One started via the this.authService.performTokenRequest() and then again via authState.performActionWithFreshTokens() if an exception is not created during the first.
This results in a response that has the initial accessToken and refreshToken in the request's response in a access_token_response along with the 2nd token request's response added as a separate access_token. Example:
{...
"access_token_response": {
"request": {
"configuration": {
"authorizationEndpoint": "https://xxx",
"tokenEndpoint": "https://zzz"
},
"clientId": "890890",
"nonce": "9AJax1TSN391wrtdRDVT_w",
"grantType": "authorization_code",
"redirectUri": "bbb",
"authorizationCode": "456456-1",
"additionalParameters": {}
},
"token_type": "Bearer",
"access_token": "THE_FIRST_ACCESS_TOKEN",
"expires_at": 1721831273530,
"id_token": "123123",
"refresh_token": "THE_FIRST_REFRESH_TOKEN",
"scope": "openid offline_access",
"additionalParameters": {}
},
"access_token": "THE_SECOND_ACCESS_TOKEN"
}
At this point, our first refreshToken is invalidated by the provider we're using and we don't have the 2nd refreshToken created during that 2nd request.
Code block in question: GenericOAuth2Plugin.java, line 438
if (oauth2Options.getAccessTokenEndpoint() != null) {
this.authService = new AuthorizationService(getContext());
TokenRequest tokenExchangeRequest;
try {
tokenExchangeRequest = authorizationResponse.createTokenExchangeRequest();
this.authService.performTokenRequest(
tokenExchangeRequest,
(accessTokenResponse, exception) -> {
authState.update(accessTokenResponse, exception);
if (exception != null) {
savedCall.reject(ERR_AUTHORIZATION_FAILED, String.valueOf(exception.code), exception);
} else {
if (accessTokenResponse != null) {
if (oauth2Options.isLogsEnabled()) {
Log.i(getLogTag(), "Access token response:\n" + accessTokenResponse.jsonSerializeString());
}
authState.performActionWithFreshTokens(
authService,
(accessToken, idToken, ex1) -> {
AsyncTask<String, Void, ResourceCallResult> asyncTask = new ResourceUrlAsyncTask(
savedCall,
oauth2Options,
getLogTag(),
authorizationResponse,
accessTokenResponse
);
asyncTask.execute(accessToken);
}
);
} else {
resolveAuthorizationResponse(savedCall, authorizationResponse);
}
}
}
);
Platform(s) Support Requested
Describe Preferred Solution
As it's openid's AuthState performActionWithFreshTokens()'s callback that's preparing the new accessToken for the ResourceUrlAsyncTask (and the eventual adding of the response and token with OAuth2Utils.assignResponses()), an option to disable the call for authState.performActionWithFreshTokens() via config value (ex: freshTokenAttemptEnabled boolean added to OAuth2Options.java or something) could be the easiest approach. Where the default would be to do the 2nd token call where a false flag would simply not attempt it.
Example config and usage:
var authConfig = {
appId: CLIENT_ID,
authorizationBaseUrl: AUTHORIZATION_ENDPOINT,
accessTokenEndpoint: TOKEN_ENDPOINT,
logoutIdentityUrl: LOGOUT_ENDPOINT
pkceEnabled: true,
responseType: 'code',
redirectUrl: REDIRECT_URL,
scope: 'openid',
ios: {
resourceUrl: ''
},
web: {
windowTarget: '_blank'
},
freshTokenAttemptEnabled: false
...
const config = { ...authConfig, additionalParameters: { culture: languageCode } };
const response = await OAuth2Client.authenticate(config);
...
Example change:
if (oauth2Options.getAccessTokenEndpoint() != null) {
this.authService = new AuthorizationService(getContext());
TokenRequest tokenExchangeRequest;
try {
tokenExchangeRequest = authorizationResponse.createTokenExchangeRequest();
this.authService.performTokenRequest(tokenExchangeRequest, (accessTokenResponse, exception) -> {
authState.update(accessTokenResponse, exception);
if (exception != null) {
savedCall.reject(ERR_AUTHORIZATION_FAILED, String.valueOf(exception.code), exception);
}
else {
if (oauth2Options.isFreshTokenAttemptEnabled()) {
if (accessTokenResponse != null)) {
if (oauth2Options.isLogsEnabled()) {
Log.i(getLogTag(), "Access token response:\n" + accessTokenResponse.jsonSerializeString());
}
authState.performActionWithFreshTokens(authService,
(accessToken, idToken, ex1) -> {
AsyncTask<String, Void, ResourceCallResult> asyncTask =
new ResourceUrlAsyncTask(
savedCall,
oauth2Options,
getLogTag(),
authorizationResponse,
accessTokenResponse);
asyncTask.execute(accessToken);
});
} else {
resolveAuthorizationResponse(savedCall, authorizationResponse);
}
} else {
resolveAuthorizationResponse(savedCall, authorizationResponse, accessTokenResponse);
}
}
});
} catch (Exception e) {
savedCall.reject(ERR_NO_AUTHORIZATION_CODE, e);
}
}
and an overload of resolveAuthorizationResponse():
private void resolveAuthorizationResponse(PluginCall savedCall, AuthorizationResponse authorizationResponse, TokenResponse tokenResponse) {
JSObject json = new JSObject();
OAuth2Utils.assignResponses(json, null, authorizationResponse, tokenResponse);
savedCall.resolve(json);
}
Describe Alternatives
Alternatives for the prevention of the 2nd tokenRequest attempt are limited outside of the mentioned block. Another alternative would be to have openid include the refreshToken in the AuthState's AuthorizationService.TokenResponseCallback() (Followed by including it with the ResourceUrlAsyncTask for the assigning). And then update OAuth2Utils.assignResponses() to also include it:
public static void assignResponses(JSObject resp, String accessToken, String refreshToken, AuthorizationResponse authorizationResponse, TokenResponse accessTokenResponse) {
// #154
if (authorizationResponse != null) {
resp.put("authorization_response", authorizationResponse.jsonSerialize());
}
if (accessTokenResponse != null) {
resp.put("access_token_response", accessTokenResponse.jsonSerialize());
}
if (accessToken != null) {
resp.put("access_token", accessToken);
}
if (refreshToken != null) {
resp.put("refreshToken ", refreshToken);
}
new AuthorizationService.TokenResponseCallback() {
@Override
public void onTokenRequestCompleted(
@Nullable TokenResponse response,
@Nullable AuthorizationException ex) {
update(response, ex);
String accessToken = null;
String idToken = null;
AuthorizationException exception = null;
if (ex == null) {
mNeedsTokenRefreshOverride = false;
accessToken = getAccessToken();
refreshToken = getRefreshToken();
idToken = getIdToken();
} else {
exception = ex;
}
...
}
});
This way, the consumer can decide to make use of either result. The ones inside of the original access_token_response or the appended tokens.
This does not seem viable.
Additional Context
This doesn't appear to be an issue with IOS. ByteowlsCapacitorOauth2.swift and the authenticate flow does not appear to have a case for attempting a request with 'fresh' tokens after the initial accessTokenJsObject or accessTokenResponse is created.
Flow type: Authentication
Platform: Android
Provider: Duende
generic-oauth2 version: 5.0.0
Describe the Feature
The ability to disable the
authState.performActionWithFreshTokens()call during the authenticate flow. For cases where the providers invalidate refreshTokens with each tokenRequest regardless of the refreshToken being used.When working with a provider that rolls new
refreshTokensanytime the token endpoints are hit, regardless of using arefreshToken, the Android implementation never gets the 2ndrefreshTokencreated. IOS doesn't appear to have this issue and does not attempt a second token request for 'fresh' tokens during authentication.The Android implementation of the
authenticatemethod attempts two separate token requests. One started via thethis.authService.performTokenRequest()and then again viaauthState.performActionWithFreshTokens()if anexceptionis not created during the first.This results in a response that has the initial
accessTokenandrefreshTokenin the request's response in aaccess_token_responsealong with the 2nd token request's response added as a separateaccess_token. Example:At this point, our first refreshToken is invalidated by the provider we're using and we don't have the 2nd
refreshTokencreated during that 2nd request.Code block in question: GenericOAuth2Plugin.java, line 438
Platform(s) Support Requested
Describe Preferred Solution
As it's openid's
AuthStateperformActionWithFreshTokens()'s callback that's preparing the new accessToken for theResourceUrlAsyncTask(and the eventual adding of the response and token withOAuth2Utils.assignResponses()), an option to disable the call forauthState.performActionWithFreshTokens()via config value (ex:freshTokenAttemptEnabledbooleanadded toOAuth2Options.javaor something) could be the easiest approach. Where the default would be to do the 2nd token call where a false flag would simply not attempt it.Example config and usage:
Example change:
and an overload of
resolveAuthorizationResponse():Describe Alternatives
Alternatives for the prevention of the 2nd
tokenRequestattempt are limited outside of the mentioned block. Another alternative would be to have openid include therefreshTokenin the AuthState'sAuthorizationService.TokenResponseCallback()(Followed by including it with theResourceUrlAsyncTaskfor the assigning). And then updateOAuth2Utils.assignResponses()to also include it:This way, the consumer can decide to make use of either result. The ones inside of the original
access_token_responseor the appended tokens.This does not seem viable.
Additional Context
This doesn't appear to be an issue with IOS. ByteowlsCapacitorOauth2.swift and the authenticate flow does not appear to have a case for attempting a request with 'fresh' tokens after the initial
accessTokenJsObjectoraccessTokenResponseis created.