Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b1a29209 authored by daquexian's avatar daquexian Committed by Vincent Breitmoser
Browse files

refactor oauth 2.0 code to support different mail servers

parent fd0576e2
Loading
Loading
Loading
Loading
+74 −11
Original line number Diff line number Diff line
package com.fsck.k9.mail.oauth;


import java.util.List;

import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.OAuth2NeedUserPromptException;

import java.util.HashMap;
import java.util.Map;


public interface OAuth2TokenProvider {
public abstract class OAuth2TokenProvider {
    /**
     * A default timeout value to use when fetching tokens.
     */
    int OAUTH2_TIMEOUT = 30000;
    public static int OAUTH2_TIMEOUT = 30000;

    private Map<String,String> authTokens = new HashMap<>();

    public void exchangeCode(String email, String code) throws AuthenticationFailedException {
        SpecificOAuth2TokenProvider specificProvider = getSpecificProviderFromEmail(email);
        Tokens tokens = specificProvider.exchangeCode(email, code);

    boolean exchangeCode(String username, String code);
        authTokens.put(email, tokens.accessToken);
        saveRefreshToken(email, tokens.refreshToken);
    }

    protected abstract void saveRefreshToken(String email, String refreshToken);

    /**
     * Fetch a token. No guarantees are provided for validity.
     * @param username Username
     * @param email Username
     * @return Token string
     * @throws AuthenticationFailedException
     * @throws AuthenticationFailedException throw when error occurs
     * @throws OAuth2NeedUserPromptException throw it when user haven't allow us to login
     */
    String getToken(String username, long timeoutMillis) throws AuthenticationFailedException, OAuth2NeedUserPromptException;
    public String getToken(String email, long timeoutMillis)
            throws AuthenticationFailedException, OAuth2NeedUserPromptException {
        if (!authTokens.containsKey(email)) {
            String refreshToken = getRefreshToken(email);
            if (refreshToken != null) {
                try {
                    refreshToken(email, refreshToken);
                } catch (Exception e) {
                    throw new AuthenticationFailedException(e.getMessage());
                }
            } else {
                showAuthDialog(email);
                throw new OAuth2NeedUserPromptException();
            }
        }
        return authTokens.get(email);
    }

    public abstract void showAuthDialog(String email);

    /**
     * Invalidate the token for this username.
     * get refresh token got before
     * @param username username (usually email address)
     * @return refresh token
     */
    protected abstract String getRefreshToken(String username);

    /**
     * refresh access token with refresh token
     * @param email email address
     * @param refreshToken refresh token got before
     * @throws AuthenticationFailedException throws it when error occurs
     */
    private void refreshToken(String email, String refreshToken) throws AuthenticationFailedException {
        SpecificOAuth2TokenProvider provider = getSpecificProviderFromEmail(email);
        String newToken = provider.refreshToken(email, refreshToken);
        authTokens.put(email, newToken);
    }

    /**
     * Invalidate the token for this email.
     *
     * <p>
     * Note that the token should always be invalidated on credential failure. However invalidating a token every
@@ -33,6 +81,21 @@ public interface OAuth2TokenProvider {
     * <p>
     * Invalidating a token and then failure with a new token should be treated as a permanent failure.
     */
    void invalidateToken(String username);
    public void invalidateAccessToken(String email) {
        authTokens.remove(email);
    }

    public abstract void invalidateRefreshToken(String username);

    protected abstract SpecificOAuth2TokenProvider getSpecificProviderFromEmail(String email);

    public static class Tokens {
        String accessToken;
        String refreshToken;

        public Tokens(String accessToken, String refreshToken) {
            this.accessToken = accessToken;
            this.refreshToken = refreshToken;
        }
    }
}
+18 −0
Original line number Diff line number Diff line
package com.fsck.k9.mail.oauth;

import com.fsck.k9.mail.AuthenticationFailedException;

public interface SpecificOAuth2TokenProvider {
    OAuth2TokenProvider.Tokens exchangeCode(String username, String code) throws AuthenticationFailedException;

    /**
     * refresh access token with refresh token
     * @param username username (usually email address)
     * @param refreshToken refresh token got before
     * @return new access token
     * @throws AuthenticationFailedException throws it when error occurs
     */
    String refreshToken(String username, String refreshToken) throws AuthenticationFailedException;

    void showAuthDialog();
}
+3 −3
Original line number Diff line number Diff line
@@ -37,7 +37,6 @@ import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.NetworkType;
import com.fsck.k9.mail.OAuth2NeedUserPromptException;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
@@ -396,7 +395,7 @@ class ImapConnection {
            return attemptXOAuth2();
        } catch (NegativeImapResponseException e) {
            //TODO: Check response code so we don't needlessly invalidate the token.
            oauthTokenProvider.invalidateToken(settings.getUsername());
            oauthTokenProvider.invalidateAccessToken(settings.getUsername());

            if (!retryXoauth2WithNewToken) {
                throw handlePermanentXoauth2Failure(e);
@@ -424,7 +423,8 @@ class ImapConnection {
            //Okay, we failed on a new token.
            //Invalidate the token anyway but assume it's permanent.
            Timber.v(e, "Authentication exception for new token, permanent error assumed");
            oauthTokenProvider.invalidateToken(settings.getUsername());
            oauthTokenProvider.invalidateAccessToken(settings.getUsername());
            oauthTokenProvider.invalidateRefreshToken(settings.getUsername());
            throw handlePermanentXoauth2Failure(e2);
        }
    }
+2 −2
Original line number Diff line number Diff line
@@ -766,7 +766,7 @@ public class SmtpTransport extends Transport {
                throw negativeResponse;
            }

            oauthTokenProvider.invalidateToken(username);
            oauthTokenProvider.invalidateAccessToken(username);

            if (!retryXoauthWithNewToken) {
                handlePermanentFailure(negativeResponse);
@@ -800,7 +800,7 @@ public class SmtpTransport extends Transport {
            //Invalidate the token anyway but assume it's permanent.
            Timber.v(negativeResponseFromNewToken, "Authentication exception for new token, permanent error assumed");

            oauthTokenProvider.invalidateToken(username);
            oauthTokenProvider.invalidateAccessToken(username);

            handlePermanentFailure(negativeResponseFromNewToken);
        }
+34 −9
Original line number Diff line number Diff line
@@ -5,7 +5,6 @@ import java.io.IOException;
import java.net.UnknownHostException;
import java.util.List;

import android.app.Activity;
import android.net.ConnectivityManager;

import com.fsck.k9.mail.AuthType;
@@ -19,6 +18,7 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.XOAuth2ChallengeParserTest;
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory;
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
import com.fsck.k9.mail.oauth.SpecificOAuth2TokenProvider;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import com.fsck.k9.mail.store.imap.mockserver.MockImapServer;
import okio.ByteString;
@@ -1010,14 +1010,14 @@ public class ImapConnectionTest {

    private OAuth2TokenProvider createOAuth2TokenProvider() {
        return new OAuth2TokenProvider() {
            private int invalidationCount = 0;
            private int accessTokenInvalidationCount = 0;

            @Override
            public String getToken(String username, long timeoutMillis) throws AuthenticationFailedException {
                assertEquals(USERNAME, username);
            public String getToken(String email, long timeoutMillis) throws AuthenticationFailedException {
                assertEquals(USERNAME, email);
                assertEquals(OAUTH2_TIMEOUT, timeoutMillis);

                switch (invalidationCount) {
                switch (accessTokenInvalidationCount) {
                    case 0: {
                        return XOAUTH_TOKEN;
                    }
@@ -1031,13 +1031,38 @@ public class ImapConnectionTest {
            }

            @Override
            public void invalidateToken(String username) {
                assertEquals(USERNAME, username);
                invalidationCount++;
            public void showAuthDialog(String email) {
                throw new UnsupportedOperationException();
            }

            @Override
            protected String getRefreshToken(String username) {
                throw new UnsupportedOperationException();
            }

            @Override
            public void invalidateAccessToken(String email) {
                assertEquals(USERNAME, email);
                accessTokenInvalidationCount++;
            }

            @Override
            public void invalidateRefreshToken(String email) {
                assertEquals(USERNAME, email);
            }

            @Override
            protected SpecificOAuth2TokenProvider getSpecificProviderFromEmail(String email) {
                return null;
            }

            @Override
            public void exchangeCode(String email, String code) {
                throw new UnsupportedOperationException();
            }

            @Override
            public boolean exchangeCode(String username, String code) {
            protected void saveRefreshToken(String email, String refreshToken) {
                throw new UnsupportedOperationException();
            }

Loading