Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: option to disable authState.performActionWithFreshTokens() flow during authenticate #270

Open
TheLyndonRay opened this issue Jul 26, 2024 · 0 comments
Labels
android enhancement New feature or request

Comments

@TheLyndonRay
Copy link

TheLyndonRay commented Jul 26, 2024

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

  • Android

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.

@TheLyndonRay TheLyndonRay changed the title Feat: Feat: option to disable authState.performActionWithFreshTokens() flow during authenticate Jul 26, 2024
@moberwasserlechner moberwasserlechner added enhancement New feature or request android labels Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
android enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants