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

Commit 70a2100f authored by daquexian's avatar daquexian
Browse files

refactor oauth 2.0 code to support different mail servers

parent 2bc248e1
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
@@ -36,7 +36,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;
@@ -381,7 +380,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);
@@ -409,7 +408,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
@@ -681,7 +681,7 @@ public class SmtpTransport extends Transport {
                throw negativeResponse;
            }

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

            if (!retryXoauthWithNewToken) {
                handlePermanentFailure(negativeResponse);
@@ -715,7 +715,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);
        }
+35 −11
Original line number Diff line number Diff line
@@ -3,9 +3,7 @@ package com.fsck.k9.mail.store.imap;

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 +17,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 +1009,14 @@ public class ImapConnectionTest {

    private OAuth2TokenProvider createOAuth2TokenProvider() throws AuthenticationFailedException {
        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;
                    }
@@ -1025,19 +1024,44 @@ public class ImapConnectionTest {
                        return XOAUTH_ANOTHER_TOKEN;
                    }
                    default: {
                        throw new AssertionError("Ran out of auth tokens. invalidateToken() called too often?");
                        throw new AssertionError("Ran out of auth tokens. invalidateAccessToken() called too often?");
                    }
                }
            }

            @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