Loading k9mail-library/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java +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 Loading @@ -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; } } } k9mail-library/src/main/java/com/fsck/k9/mail/oauth/SpecificOAuth2TokenProvider.java 0 → 100644 +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(); } k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapConnection.java +3 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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); } } Loading k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java +2 −2 Original line number Diff line number Diff line Loading @@ -681,7 +681,7 @@ public class SmtpTransport extends Transport { throw negativeResponse; } oauthTokenProvider.invalidateToken(username); oauthTokenProvider.invalidateAccessToken(username); if (!retryXoauthWithNewToken) { handlePermanentFailure(negativeResponse); Loading Loading @@ -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); } Loading k9mail-library/src/test/java/com/fsck/k9/mail/store/imap/ImapConnectionTest.java +35 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; } Loading @@ -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 Loading
k9mail-library/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java +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 Loading @@ -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; } } }
k9mail-library/src/main/java/com/fsck/k9/mail/oauth/SpecificOAuth2TokenProvider.java 0 → 100644 +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(); }
k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapConnection.java +3 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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); } } Loading
k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java +2 −2 Original line number Diff line number Diff line Loading @@ -681,7 +681,7 @@ public class SmtpTransport extends Transport { throw negativeResponse; } oauthTokenProvider.invalidateToken(username); oauthTokenProvider.invalidateAccessToken(username); if (!retryXoauthWithNewToken) { handlePermanentFailure(negativeResponse); Loading Loading @@ -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); } Loading
k9mail-library/src/test/java/com/fsck/k9/mail/store/imap/ImapConnectionTest.java +35 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; } Loading @@ -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